Esc

    Protocol

    Venue reports, books and funding

    The signed VenueReport, BookUpdate and FundingUpdate payloads, how books travel as snapshots and deltas, and the golden vectors that freeze every layout.

    A price update says what a node concluded. A node also signs three more payloads:

    • A venue report says what the node saw for each source of one feed: the book, the perp data, and how the source counted in the price.
    • A book update carries one venue instrument's local book, so that a client can keep its own copy.
    • A funding update carries one perpetual's funding, as its venue states it.

    All use the same key, digest and signature format as a price update. See Signing and the price payload. A node builds venue reports and book deltas only for clients that ask. See Subscriptions.

    Common rules

    • All integers are big-endian. The version byte is 1.
    • Each payload is canonical. A decoder refuses any other byte form, so each report or update has exactly one encoding. Trailing bytes are an error.
    • Each frame is frame_type ‖ signer address[20] ‖ payload ‖ signature[65]. The signer address is an unauthenticated routing hint, as in a price frame.
    Frame type Payload Kind
    1 PriceUpdate 0x01
    2 VenueReport 0x03
    3 BookUpdate 0x04
    4 FundingUpdate 0x05

    Venue codes are the same as the bits of the source bitmap:

    Code Venue
    0 Binance
    1 Bybit
    2 OKX
    3 Coinbase
    4 Kraken
    5 Deribit

    Market codes:

    Code Market
    0 spot
    1 linear perpetual
    2 inverse perpetual

    A decoder refuses an unknown venue or market code.

    VenueReport v1

    Kind 0x03. The payload is a 55-byte header and n entries of 95 bytes each.

    Offset Size Field Type Meaning
    0 1 version u8 1
    1 1 kind u8 0x03
    2 32 feed_id bytes32 The feed.
    34 8 publish_time_ns u64 When the node signed the report, unix nanoseconds.
    42 8 seq u64 For each (node, feed) pair. It is a separate counter from the price seq.
    50 4 expo i32 The feed's exponent, for every price in the entries.
    54 1 n u8 The number of entries. One entry for each configured source, in config order.

    Entry

    Each entry is 96 + L bytes, where L is the length of its symbol. Offsets are from the start of the entry.

    Offset Size Field Type Meaning
    0 1 venue u8 Venue code.
    1 1 market u8 Market code.
    2 1 L u8 Symbol length, 1–32.
    3 L symbol ASCII The venue's own symbol for the instrument, such as BTCUSDT.
    3 + L 1 status u8 How the source counted. See Source status codes.
    4 + L 2 present u16 Presence bits. Bit i is set when slot i holds a value.
    6 + L 2 weight_bps u16 The source's share of the total weight of the price, 0–10,000. It is 0 unless the source was used.
    8 + L 80 slots 10 × 8 bytes Ten optional values. An absent value is all zeros.
    88 + L 8 local_recv_ns u64 The node's receipt time of the newest observation, or 0 with no data.

    The ten slots, in order:

    Slot Presence bit Value Type Unit
    0 0x0001 best bid i64 price at expo
    1 0x0002 best ask i64 price at expo
    2 0x0004 bid size u64 base units at exponent −8
    3 0x0008 ask size u64 base units at exponent −8
    4 0x0010 impact bid i64 price at expo
    5 0x0020 impact ask i64 price at expo
    6 0x0040 mark i64 price at expo
    7 0x0080 index i64 price at expo
    8 0x0100 funding i64 the venue's own rate over its own interval, at exponent −9
    9 0x0200 venue timestamp u64 unix nanoseconds, venue clock

    Bits 10–15 of present must be 0.

    Source status codes

    Code Status Meaning
    0 used The source is in the price, with a weight.
    1 outlier Outlier rejection dropped the source.
    2 stale The newest data of the source is older than the staleness limit of the feed.
    3 crossed The book of the source is crossed.
    4 no data No data yet, or none since its connection dropped.
    5 invalid The data cannot be priced, for example a book price of zero.
    6 feed halted The source had a usable quote, but the feed halted.
    7 peg unavailable The source quotes in the feed's peg asset, and the peg has no usable price, so it dropped out.

    Report rules

    • Prices before conversion. The prices are the venue's own, before any peg conversion. The status and the weight refer to the converted price.
    • Impact prices. The impact prices are for the feed's impact_notional, from the node's local book. A book that runs out of levels gives no impact price, because a partial fill shows a tighter market than the book has.
    • Status. The status comes from the last evaluation of the feed. See Aggregation.
    • Symbol. The symbol names the instrument, so a stored report joins the venue's instrument data without the node's config. It is 1–32 bytes of printable ASCII without spaces.
    • Canonical form. A decoder refuses an unknown venue, market or status code, a bad symbol, an unknown present bit, a non-zero absent slot, and a weight above 10,000.
    • Cadence. One report after new input, at most one a second, and one at each heartbeat. The node builds and signs a report only for a feed that some client subscribed to. The first report goes out at the next evaluation after a client subscribes.
    • Sequence. The node seeds the report seq from its clock in milliseconds at startup, as for prices.

    Report size bounds

    Item Minimum Maximum
    Entries n 0 32
    Entry, 96 + L bytes 97 128
    Payload 55 4,151
    Frame, payload + 86 bytes 141 4,237

    A feed with more than 32 sources is a config error.

    Verify a report

    A consumer verifies a report like a price update: the signature, the registry, the clock and its own seq for each (node, feed) pair. The coherence check is different. Each entry's local_recv_ns must not be later than publish_time_ns. Each node reports what it saw, so a consumer does not combine reports across nodes.

    BookUpdate v1

    Kind 0x04. A book update is a snapshot of the top 50 levels on each side, or a delta: the levels that changed since the previous frame.

    The symbol has a variable length L, so the offsets after it depend on L.

    Offset Size Field Type Meaning
    0 1 version u8 1
    1 1 kind u8 0x04
    2 1 venue u8 Venue code.
    3 1 market u8 Market code.
    4 1 L u8 Symbol length, 1–32.
    5 L symbol ASCII The venue's own symbol, such as BTCUSDT.
    5 + L 8 publish_time_ns u64 When the node signed the update, unix nanoseconds.
    13 + L 8 seq u64 For each (node, book) pair. A delta applies to the frame with seq − 1.
    21 + L 1 flags u8 Bit 0: a snapshot. Bit 1: venue_seq holds a value. Other bits are 0.
    22 + L 4 price_expo i32 Always −8 in v1.
    26 + L 4 size_expo i32 Always −8 in v1.
    30 + L 8 exchange_time_ns u64 The venue's timestamp on the newest book data, or 0 when the venue sends none.
    38 + L 8 local_recv_ns u64 The node's receipt time of the newest book data.
    46 + L 8 venue_seq u64 The venue's update id of the newest book data, or 0 when bit 1 is clear.
    54 + L 1 bid count u8 At most 128.
    55 + L 1 ask count u8 At most 128.
    56 + L 16 each levels price i64, size u64 The bids from the highest price, then the asks from the lowest price.

    The payload length is 56 + L + 16 × (bids + asks) bytes.

    Book rules

    • Symbol. 1–32 bytes of printable ASCII without spaces (0x21–0x7E).
    • Exponents. A decoder refuses a price or size exponent other than −8.
    • Order. Prices are strictly ordered on each side: bids descend, asks ascend. No price repeats.
    • Snapshot. A snapshot has no level with size 0.
    • Delta. A delta lists each level whose size changed, with the new size. It lists each level that left the top 50 with size 0. Size 0 removes the level.
    • Counts. A delta of a 50-level window changes at most 100 levels on a side. The limit of 128 covers that.
    • Venue seq. It names the exact venue book state that the node signed: Binance and Bybit u, OKX seqId, Deribit change_id. Two nodes' books of one instrument thus fall on one venue timeline. Coinbase and Kraken send no update id. A zero id is real on some venues, so bit 1, not a zero, marks an absent id. An empty snapshot after a drop has none.

    Book size bounds

    Item Minimum Maximum
    Symbol length L 1 32
    Levels on each side 0 128
    Payload 57 4,184
    Frame, payload + 86 bytes 143 4,270

    How books travel

    The node keeps a local book for each venue instrument that reads depth. That is a source of a feed with impact_notional set and book_top in its kinds. The node builds the book from a venue snapshot and the changes after it, and it proves that each change follows the last one. A failed proof is a resync: the node drops the connection and starts from a fresh snapshot. See Aggregation.

    The node then sends the book to clients in its own format:

    Rule Value
    Depth The top 50 levels on each side.
    Snapshot Every second for each book, whether or not a client subscribed. The next subscriber thus gets a recent snapshot at once.
    Snapshot after a disconnect At once after the venue connection drops. This snapshot is empty until new data arrives.
    Delta The levels that changed since the node's last frame for that book. The node computes it from its own last frame, not from the venue's messages. Thus the format is the same for every venue.
    Rate cap At most one frame for each book every 50 ms.
    Delta demand The node builds a delta only for a book that some client subscribed to. A skipped delta takes no seq.
    Sequence One counter for each (node, book) pair, shared by snapshots and deltas. The node seeds it from its clock in milliseconds at startup.

    A client that applies a snapshot, and then each delta in seq order, holds exactly the node's top 50 levels.

    Keep a book on the client

    Do these steps for each (node, book) pair:

    1. Verify the frame: signature, registry, clock, and a seq greater than the last one.
    2. If the frame is a snapshot, replace the book with its levels.
    3. If the frame is a delta, apply it only when its seq is the last seq plus 1. For each level, set the new size. Remove the level when the size is 0.
    4. If a seq is missing, mark the book invalid. Ignore deltas until the next snapshot.
    5. An empty snapshot means that the venue connection dropped or has no data yet. Do not use that book.

    Keep one book for each node. Never mix the levels of two nodes in one book. The Rust SDK keeps one book for each node and returns the freshest valid one. See Rust SDK.

    A client that subscribes gets the latest snapshot and the deltas after it at once. See Fanout and backpressure.

    FundingUpdate v1

    Kind 0x05. A funding update carries one perpetual's funding. Nothing is converted: the rate is a fraction over the venue's own basis, and open interest is in the venue's own unit.

    Offset Size Field Type Meaning
    0 1 version u8 1
    1 1 kind u8 0x05
    2 1 venue u8 Venue code.
    3 1 market u8 Market code.
    4 1 L u8 Symbol length, 1–32.
    5 L symbol ASCII The venue's own symbol, such as BTC-USDT-SWAP.
    5 + L 8 publish_time_ns u64 When the node signed the update, unix nanoseconds.
    13 + L 8 seq u64 For each (node, perpetual) pair.
    21 + L 1 flags u8 A field holds a value: bit 0 predicted rate, bit 1 interval, bit 2 next funding time, bit 3 settled rate, bit 4 open interest.
    22 + L 1 basis u8 0: the fraction paid at each funding time. 1: the fraction over 8 hours (Deribit funding_8h).
    23 + L 1 open_interest_unit u8 0 base asset, 1 contracts, 2 quote currency. 0 when bit 4 is clear.
    24 + L 4 rate_expo i32 Always −12 in v1.
    28 + L 4 open_interest_expo i32 Always −8 in v1.
    32 + L 8 rate i64 The current rate.
    40 + L 8 predicted_rate i64 The venue's prediction of the next rate.
    48 + L 8 interval_ns u64 The length of one funding period.
    56 + L 8 next_funding_time_ns u64 When the current rate is paid.
    64 + L 8 settled_rate i64 The last rate that the venue settled.
    72 + L 8 settled_time_ns u64 When the settled rate was paid.
    80 + L 8 open_interest u64
    88 + L 8 exchange_time_ns u64 The venue's timestamp on the newest funding data, or 0 when the venue sends none.
    96 + L 8 local_recv_ns u64 The node's receipt time of the newest funding data.

    The payload length is 104 + L bytes: 105 to 136. A frame is 191 to 222 bytes.

    Funding fields by venue

    The node takes the fields from messages that it already reads.

    Venue, channel Rate Predicted Interval Next time Settled Open interest
    Binance perp, markPrice r – – T – –
    Bybit perp, tickers fundingRate – fundingIntervalHour nextFundingTime – openInterest, base
    OKX swap, funding-rate fundingRate nextFundingRate nextFundingTime − fundingTime fundingTime settFundingRate at prevFundingTime, once settState is settled –
    Kraken Futures, ticker relative_funding_rate relative_funding_rate_prediction 1 hour next_funding_rate_time – openInterest, contracts
    Deribit perp, ticker funding_8h, basis 1 – – – – open_interest, USD for an inverse contract

    Funding rules

    • Flags. A field whose flag is clear is 0, and so is the unit without open interest. A decoder refuses anything else.
    • Whole state. Each frame carries every field that the venue sent, so a client needs only the latest frame. Bybit sends only the changed fields; the node keeps the last value of each until the connection drops.
    • Cadence. A frame goes out on a change, at most once a second for each perpetual, and at least once a minute. The node signs frames whether or not a client subscribed. After a venue connection drops, it signs nothing for that venue's perpetuals until new data arrives.
    • Age. A new subscriber gets the latest frame at once, and that frame can be a minute old. A client accepts a funding frame up to two minutes old.
    • Precision. A rate keeps 12 decimal places. OKX sends 16; the last 4 round away.
    • Binance interval. The markPrice stream does not state the interval, so a Binance frame has no interval_ns. Binance's fundingInfo REST endpoint gives it.

    Golden vectors

    The oracle-wire crate holds golden.json, in its tests/vectors folder. The file freezes the protocol: payload bytes, digests, signatures, frames, feed ids, the registry encoding, venue reports, book updates and funding updates. A change in this file means that the wire format changed and that old signatures no longer verify.

    Section Contents
    domain name, version, chain_id and the separator.
    feed_ids 9 symbols, each with its expo and feed_id.
    price_updates 5 vectors: btc_usd_healthy, btc_usd_degraded_synthetic, btc_usd_halted, funding_rate_negative, boundary_extremes.
    venue_reports 3 vectors: btc_usdt_three_venues, boundary_extremes, no_entries.
    book_updates 3 vectors: binance_snapshot, okx_swap_delta, boundary_extremes.
    funding_updates 3 vectors: okx_swap_settled, deribit_eight_hours, boundary_extremes.
    registry A signed registry document, its signing bytes and its digest.

    Each payload vector has these fields:

    Field Meaning
    name The case.
    signer The address that signed.
    update or report The decoded fields, as JSON.
    payload The canonical payload, 0x-hex.
    digest The domain digest of the payload.
    signature r ‖ s ‖ v, 0x-hex.
    frame The full frame, 0x-hex.

    Each vector also carries the fixed test key that signed it. That key is a public fixture. Never use it for anything real.

    In the decoded fields, each 64-bit value is a decimal string. Each byte string is 0x-hex. Venues, markets and statuses are names, such as binance, linear_perp and used. An absent optional value is null.

    Check an independent encoder

    Use the vectors to test an encoder in another language:

    1. Compute the domain separator from its definition. Compare it with domain.separator.
    2. For each entry in feed_ids, compute keccak256 of the symbol. Compare it with feed_id.
    3. For each vector, encode the decoded fields with your encoder. Compare the bytes with payload.
    4. Compute the digest of payload. Compare it with digest.
    5. Build frame_type ‖ signer ‖ payload ‖ signature. Compare it with frame.
    6. Recover the signer from signature and digest. Compare it with signer.
    7. Decode payload with your decoder. Compare the fields, then encode them again and compare the bytes.

    The signatures are deterministic. cast wallet sign --no-hash with the vector's test key gives the same bytes.

    The project checks the vectors with a second implementation. check_layout.py, next to golden.json, packs each payload with Python struct from the layout tables and hashes it with cast keccak. It shares no code with the Rust encoder. Other independent checks:

    • published Keccak-256 test vectors, which tell Keccak-256 from SHA3-256;
    • the canonical Ethereum address of secret key 1;
    • the domain separator, recomputed by foundry from the EIP-712 definition alone;
    • full signatures, compared with cast wallet sign --no-hash.

    The foundry and Python checks skip themselves when cast or python3 is not on PATH.

    To regenerate the vectors after a deliberate format change, run this command and read the diff:

    bash
    REGENERATE_GOLDEN=1 cargo test -p oracle-wire --test golden

    The two secp256k1 backends must agree byte for byte. This command runs the vectors under k256:

    bash
    cargo test -p oracle-wire --features pure-rust