Skip to content
ZIPs.tools

draft-arya-jvff-p2p-quic-transport

Version 2 Zcash P2P Network Protocol

  • Draft
Proposal metadata
ID
draft-arya-jvff-p2p-quic-transport
Status
Draft
Category
Network
Owners
  • Arya
  • Janito
Created
2026-07-31
License
MIT
Contents
  1. Connection Handshake
  2. Protocol Versioning
  3. Request Stream Types
  4. Announcement Stream Types
Name Bit Description
NODE_NETWORK 0 The node is capable of serving the complete block chain.
NODE_TREE_ROOTS 3 The node maintains a per-block note commitment tree root index and serves get-tree-roots requests.
NODE_SYNC_ARTIFACTS 4 The node serves content-addressed synchronization artifacts via get-object requests.
NODE_NETWORK_LIMITED 10 The node is capable of serving at least the most recent 2,304 blocks (approximately two days).

A node that does not hold the complete block chain — because it synchronized from a state snapshot and has not finished backfilling, or because it prunes historical blocks — MUST NOT advertise NODE_NETWORK; it SHOULD advertise NODE_NETWORK_LIMITED if it can serve at least the most recent 2,304 blocks of its best chain. A node selecting peers to request historical blocks from uses these flags (see 13).

Bits 24–31 remain reserved for temporary experiments, as in ZIP 204 24.

A node MUST ignore service bits that it does not recognize, and SHOULD preserve them when relaying address records (see Address Relay).

The legacy NODE_BLOOM flag (bit 2) is retired along with BIP 37 Bloom filtering, and MUST NOT be advertised.

Connection Handshake

Peers perform an application handshake immediately after the transport connection is established.

Handshake Sequence

  1. The initiator opens a bidirectional stream with stream type 0x00 (the handshake stream) and sends an init record.
  2. The responder sends its own init record on the same stream.
  3. The handshake is complete for a peer once it has both sent and received an init record. The negotiated protocol version is min(local_version, remote_version).

The two init records are independent: the responder MAY send its init record as soon as it observes the handshake stream's type byte, without waiting for the initiator's init record to arrive.

Only the initiator may open the handshake stream, and a connection has exactly one: a handshake stream opened by the responder, or a second handshake stream, is a connection error of type PROTOCOL_ERROR.

A node MUST NOT open any other stream before it has sent its init record, and MUST NOT send announcement records or requests before the handshake is complete. A node MAY buffer streams received from a peer before the handshake completes, or MAY refuse them with REFUSED (see Stream Types); it MUST NOT process them before the handshake completes.

The handshake stream remains open for the life of the connection. Future record kinds on the handshake stream may be defined by later revisions; a node MUST ignore handshake-stream records whose kind it does not recognize. Finishing or resetting the handshake stream signals intent to disconnect; a node observing this SHOULD close the connection with NO_ERROR.

Records on the handshake stream use the encoding of Records. The record payload begins with a 1-byte record kind; kind 0x00 is the init record.

Init Record

The init record payload (following the kind byte) has the following format:

Size Field Description
4 version The sender's advertised protocol version (uint32).
varies services Service flags (CompactSize-encoded uint64).
8 nonce Random nonce for self-connection detection (uint64).
varies user_agent User agent string (CompactSize-prefixed, max 256 bytes).
4 start_height Best block height known to the sender (uint32).
1 relay Whether the sender wants transaction relay (uint8; 0 or 1).
1 announce Whether the sender requests high-bandwidth compact block announcements (uint8; 0 or 1). See Compact Block Relay.
1 full_ids Whether the sender requests full transaction IDs in compact block announcements (uint8; 0 or 1). See Full Transaction IDs.

All fields are mandatory. The legacy timestamp, addr_recv, and addr_from fields are removed.

The nonce field is used for self-connection detection. If a node receives an init record containing a nonce it recently sent in its own init records, it MUST close the connection with the SELF_CONNECTION error code.

If relay is 0, the peer MUST NOT open a transaction announcement stream to the sender, and SHOULD NOT announce transactions to it by any other means.

There is no mechanism for changing the value of the relay, announce, or full_ids fields during the life of a connection; a node that wants to change them disconnects and performs a new handshake.

Handshake Validation

A receiving node MUST validate the init record as follows:

  • The version field MUST be at least the minimum protocol version of this ZIP (see Protocol Versioning), and at least the protocol version associated with the current network epoch (see Network Upgrade Epoch Enforcement). On failure, the node MUST close the connection with the OBSOLETE error code.
  • The nonce is checked for self-connection as specified in Init Record.
  • The user_agent string MUST NOT exceed 256 bytes.
  • The relay, announce, and full_ids fields MUST each be 0 or 1.
  • A peer MUST NOT send more than one init record per connection.

Except where another error code is specified above, a node MUST close the connection with the PROTOCOL_ERROR error code if any of these checks fail.

Handshake Example (non-normative)

Connection establishment over the QUIC transport:

Initiator                                     Responder
   ═══ QUIC handshake (ALPN "zcash/main") ═══
   ── open stream 0x00: type byte, init ────▶
   ◀──────── same stream: init ──────────────
        (handshake is complete for each peer once it
         has both sent and received an init record)
   ── open announcement streams 0x10..0x12 ─▶
   ◀─ open announcement streams 0x10..0x12 ──
   ...requests and announcements flow in both directions...

Protocol Versioning

Protocol versions are 32-bit integers, advertised in the version field of the init record. The negotiated protocol version of a connection is the minimum of the two peers' advertised versions. The negotiated protocol version determines whether the peer meets the minimum version requirements for the current network epoch (see Network Upgrade Epoch Enforcement), and gates any features that later revisions of this protocol introduce under new protocol versions.

Protocol versions inhabit a single numbering space shared with the legacy protocol: a protocol version is assigned to a (network, network upgrade) pair, not to a P2P protocol revision, and denotes the same version whether it is advertised in a legacy version message or in an init record. The association of protocol versions with network upgrades, and the procedure by which future network upgrades are assigned protocol versions, are specified in ZIP 204 25 and are not duplicated here.

At the time of writing, the current protocol version — advertised by nodes implementing the legacy protocol — is 170160 (PROTOCOL_VERSION). The protocol version from which the protocol specified by this ZIP is deployed has not yet been assigned; it will be the protocol version that ZIP 204's assignment procedure 25 assigns to the network upgrade that deploys this protocol (see Deployment). That version is the minimum peer protocol version of this protocol: every node implementing this ZIP necessarily advertises at least that version, and version-gated features of the legacy protocol (such as MSG_WTX relay and addrv2 support) are unconditionally in effect.

Network Upgrade Epoch Enforcement

Each network upgrade defines a minimum protocol version. When a network upgrade activates (as defined in ZIP 200 3), a node MUST disconnect any peer whose negotiated protocol version is less than the protocol version associated with the current epoch, using the OBSOLETE error code.

The protocol versions associated with network upgrades on each network are tabulated in ZIP 204 26.

In the preference window before an upgrade's activation, a node SHOULD preferentially connect to peers advertising the upcoming epoch's protocol version, as specified in ZIP 201 27.

Request Stream Types

This section defines each request stream type. For each type, the request and response formats and their semantics are specified. All requests and responses follow the lifecycle of Request Streams.

get-headers

Stream type: 0x01

Request:

Size Field Description
varies locator_count Number of block locator hashes (CompactSize).
varies locator_hashes Block locator hashes (each 32 bytes), from highest to lowest height.
32 hash_stop Hash of the last desired header, or all zeros to request as many as possible.
1 tx_ids Whether each returned header should be accompanied by the block's coinbase transaction and transaction IDs (uint8; 0 or 1).

Response:

Size Field Description
varies count Number of entries (CompactSize).
varies entries count entries, each encoded as follows.

Each entry:

Size Field Description
varies header The block header, encoded as a CompactSize length prefix followed by the serialized header 12 (including the Equihash solution).
1 has_txs Present only if the request had tx_ids = 1. 0x01: the coinbase transaction and transaction IDs follow. 0x00: nothing further follows for this entry.
varies coinbase Present only if has_txs is 0x01: the block's coinbase transaction, encoded as a CompactSize length prefix followed by the serialized transaction 11.
varies ids_count Present only if has_txs is 0x01: the number of transaction IDs (CompactSize).
varies ids Present only if has_txs is 0x01: the full transaction IDs (64 bytes each; see Full Transaction IDs) of the block's transactions other than the coinbase transaction, in block order.

Requests block headers starting after the first block locator hash found in the responder's best chain, up to and including hash_stop or 160 headers (MAX_HEADERS_RESULTS), whichever comes first. The locator_count MUST NOT exceed 101, and tx_ids MUST be 0 or 1.

If none of the locator hashes are found in the responder's best chain, headers start at height 1 (the block after the genesis block). If locator_count is 0, the response consists solely of the entry for the header whose hash is hash_stop, if the responder has it. A response with count = 0 is valid and indicates that the responder has no headers to return.

If tx_ids is 1, each entry additionally identifies the block's transactions: the coinbase transaction in full (it is never otherwise relayed), and every other transaction by its full transaction ID, letting the requester fetch only the transactions it is missing via get-tx (see Relay Protocol). A responder that cannot supply a returned header's transactions sets has_txs to 0x00 for that entry, and the requester falls back to get-blocks.

A node performing bulk synchronization SHOULD set tx_ids to 0: the transaction-ID form pays off only near the chain tip, where the requester already holds most of a block's transactions. Because the ids array is not individually length-prefixed, the record payload limit of Records does not bound it, and a tx_ids = 1 request over large historical blocks would otherwise compel a response of hundreds of megabytes from a request of a few dozen bytes. Both directions are therefore bounded:

  • An entry's ids_count MUST NOT exceed 65,536, the greatest number of transactions a block can contain (see Compact Block Encoding). A larger value is a connection error of type FLOOD.
  • A responder SHOULD bound the total serialized size of its response, and MAY set has_txs to 0x00 for any entry, or return fewer entries, to stay within that bound; the requester falls back to get-blocks for those entries.

The response count MUST NOT exceed 160, and MUST NOT exceed the number of headers the request could have selected. The headers MUST form a contiguous chain (each header's hashPrevBlock must match the hash of the preceding header). A node receiving a response with more than 160 headers, or with non-contiguous headers, SHOULD discard the response and assign a misbehavior penalty (see Misbehavior and Banning); this is the handling of such responses, in place of the connection error that Request Streams would otherwise prescribe.

The legacy always-zero transaction count that followed each header in headers messages is removed.

get-blocks

Stream type: 0x02

Request:

Size Field Description
varies count Number of block hashes (CompactSize).
varies hashes count block hashes (each 32 bytes).

Response: For each requested hash, in request order:

Size Field Description
1 result 0x00: a full block follows. 0x02: not found (nothing follows for this entry).
varies object If result is 0x00: a CompactSize length prefix followed by the serialized block (see Serialized Blocks).

Requests full blocks by hash; the hashes to request are learned from headers, announcements, or get-hashes responses. The count MUST NOT exceed 128. Compact blocks cannot be requested (see Compact Block Relay).

Each delivered block MUST be the block that was requested: the header of the block delivered for an entry MUST hash to that entry's requested hash. A delivered block that does not is a connection error of type PROTOCOL_ERROR — the check is by hashing alone, so a violation is never attributable to a different chain view. Without this rule a peer could answer every entry with some other valid block, choosing which of two competing blocks the requester sees while remaining syntactically conformant.

A count of 128 bounds the number of blocks but not their size, and blocks differ in size by three orders of magnitude across the chain's history. A responder SHOULD therefore also apply a byte budget, and MAY finish its sending direction after any complete entry, having answered fewer entries than were requested; the requester re-requests the remainder from any peer. For bulk synchronization, get-block-range — which is byte-bounded by construction — is the appropriate primitive (see 13).

get-tx

Stream type: 0x03

Request:

Size Field Description
varies count Number of transaction references (CompactSize).
varies txrefs count transaction references (see Transaction References). All three reference types are permitted.

Response: For each requested reference, in request order:

Size Field Description
1 result 0x00: a transaction follows. 0x02: not found (nothing follows for this entry).
varies tx If result is 0x00: a CompactSize length prefix followed by the serialized transaction 11.

Requests transactions by reference. The count MUST NOT exceed 50,000.

Each delivered transaction MUST be the transaction that was requested: it MUST match the corresponding entry's reference, under the identifier that reference names (txid, wtxid, or the short transaction ID relative to the named block). A delivered transaction that does not is a connection error of type PROTOCOL_ERROR.

As with get-blocks, the count bound does not bound the response in bytes: 50,000 references occupy a request of a few megabytes but could name transactions totalling far more, an amplification the count limit alone does not address. A responder SHOULD apply a byte budget to the response, and MAY finish its sending direction after any complete entry; the requester re-requests what it still needs. A requester SHOULD size a request to the bytes it is prepared to receive rather than to the count limit.

SHORTID references identify a transaction within a block relayed by a compact block; their handling is specified in Requesting Missing Transactions.

get-addr

Stream type: 0x04

Request: Empty (the stream is finished immediately after the stream type byte).

Response:

Size Field Description
varies count Number of address records (CompactSize).
varies addresses count network address records (see Network Address Record).

Requests peer addresses from the remote node. The response count MUST NOT exceed 1000; a node receiving a larger response SHOULD discard the response and assign a misbehavior penalty (see Misbehavior and Banning); this is the handling of such responses, in place of the connection error that Request Streams would otherwise prescribe.

Each returned record carries the address's last-known service flags in its services field (see Service Flags and Network Address Record). A responder SHOULD set that field to the services it last observed the address advertise in an init record, or the services the record was relayed to it with if it has never connected to the address. The requester MAY use the field to select candidate peers offering the services it needs (for example, NODE_SYNC_ARTIFACTS or NODE_TREE_ROOTS), but relayed service flags are an unauthenticated claim: the peer's actual services are established only by the services field of its own init record upon connecting, and a node MUST NOT treat an address book entry's flags as more than a selection hint.

To impede address-based fingerprinting attacks, a node SHOULD send get-addr only on outbound connections, at most once per connection, and SHOULD only answer get-addr requests on inbound connections (resetting its sending direction of the stream with REFUSED otherwise).

get-mempool

Stream type: 0x05

Request: Empty (the stream is finished immediately after the stream type byte).

Response: Open-ended. The response is a sequence of records (see Records), each a single transaction reference (see Transaction References) of type TXID or WTXID; each field of a reference is fixed in size for its type. SHORTID references MUST NOT be used.

Subscribes to the contents of the peer's transaction memory pool. The responder first sends a record for each transaction currently in its mempool (the snapshot; no snapshot records indicate an empty mempool). It then keeps its sending direction open, and sends further records referencing transactions as they are accepted into its mempool, until the stream or connection ends. The requester fetches transactions of interest with get-tx.

This is the one stream type whose response is open-ended (see Request Streams): the responder finishing its sending direction ends the subscription (for example, on shutdown), and a requester that no longer wants the subscription cancels the responder's sending direction with CANCELLED. A node MUST NOT open more than one concurrent get-mempool stream to the same peer; a second concurrent subscription is a connection error of type PROTOCOL_ERROR.

Records after the snapshot SHOULD be subject to the same trickling delay as transaction announcements (see Trickling). The responder MAY omit references it has already sent to the same peer — in the snapshot, in an earlier record, or on a transaction announcement stream — and the requester MUST tolerate duplicate references.

Serving the snapshot costs the responder a full serialization of its mempool for a request of one byte, so a node SHOULD rate-limit get-mempool per peer — a peer that repeatedly cancels and re-opens the stream is requesting that work again each time — and SHOULD decline the request from a peer for which it has set relay = 0 in its init record, since such a peer has declined transaction relay in the other direction. A node concerned about topology inference SHOULD apply the trickling delay of Trickling to the post-snapshot records, as above; without it the subscription reports each transaction's arrival at the responder without delay, which is exactly what trickling exists to obscure.

get-hashes

Stream type: 0x06

Request:

Size Field Description
4 start_height Height of the first requested block hash (uint32, little-endian).
4 count Maximum number of block hashes requested (uint32, little-endian).

Response:

Size Field Description
varies count Number of entries (CompactSize).
varies entries count entries, each encoded as follows.

Entry k, for k from 0 to count − 1, describes the block at height start_height + k of the responder's best chain:

Size Field Description
32 hash Hash of the block at height start_height + k.
varies size The block's serialized size in bytes (see Serialized Blocks) (CompactSize).
varies txs Number of transactions in the block, including the coinbase transaction (CompactSize).
varies txouts Number of transparent outputs created by the block's transactions, including coinbase outputs (CompactSize).
varies notes Number of note commitments added by the block: two per JoinSplit description, plus one per Sapling Output description, plus one per Orchard-protocol Action description (in the Orchard or, from NU6.3, the Ironwood pool) (CompactSize).

Requests the hashes of count consecutive blocks of the responder's best chain starting at start_height, together with per-block sync-cost metadata. A requester that needs coarser granularity (for example, the spacing of its checkpoints; see 13) aggregates the per-block values itself.

The size, txs, txouts, and notes fields are scheduling hints: they let a synchronizing node estimate, before downloading, the download volume and the validation and note-commitment-tree work in each range of the chain, and so divide download work across peers and interleave transfer with computation to reach the tip fastest (see 13). Cumulative txouts values additionally locate each block's bits within the spentness-hint bitmap of 13. They are deterministic functions of the block — for a given hash, every honest responder returns identical values. A node MUST NOT rely on them for any consensus or validity purpose.

The requested count MUST NOT exceed 50,000, and the greatest requested height (start_height + count − 1) MUST NOT exceed 0xFFFFFFFF.

The response count MUST NOT exceed the requested count, MAY be less, and MAY be 0: a node omits requested heights above its chain tip, and SHOULD NOT include entries for blocks within 100 blocks of its chain tip (which could still be affected by a chain reorganization; the same margin as the reorganization rule of Checkpointed Synchronization). Truncation MUST be a prefix: if fewer entries are returned than were requested, the returned entries MUST correspond to the lowest requested heights, so that the requester can re-request the missing tail.

Responses are not self-authenticating: a node MUST NOT rely on the returned hashes for any security-relevant purpose until they are verified, per the untrusted-inputs rule of Checkpointed Synchronization. The metadata, by contrast, is checkable after the fact: once a node has downloaded an entry's block, it SHOULD verify any hints it relied upon, and SHOULD assign a misbehavior penalty (see Misbehavior and Banning) if txs or notes do not match the downloaded block. Those two fields are determined by the block hash alone, so a mismatch is not attributable to a different chain view.

size is not. A block's serialized size depends on its authorizing data — notably scriptSig lengths — which the txid, and hence the block hash, does not commit to (see get-block-range and ZIP 244 28). A node that obtained the hints from one peer and the blocks from another therefore MUST NOT penalize a size mismatch unless it has verified the delivered blocks' authorizing data commitment; otherwise a peer that pads the blocks it serves would cause the penalty to fall on the honest peer that served the hints, which is an eviction primitive rather than a defense. A size mismatch on unverified blocks is instead grounds to re-fetch the blocks.

get-block-range

Stream type: 0x07

Request:

Size Field Description
32 final_hash Hash of the highest block in the requested range.
4 count Maximum number of blocks requested (uint32, little-endian). MUST NOT exceed 65,536.
4 max_bytes Maximum total serialized size of the delivered blocks, in bytes (uint32, little-endian). MUST NOT exceed 67,108,864 (64 MiB).

Response:

Size Field Description
1 result 0x00: blocks follow. 0x02: not found (the responder does not have the block with hash final_hash; nothing follows).
varies blocks A sequence of serialized blocks, each encoded as a CompactSize length prefix followed by the serialized block (see Serialized Blocks), in descending height order: the first block is the block whose hash is final_hash, and each subsequent block is the parent of the previous one.

get-block-range is the bulk block download primitive: where get-blocks requests individual blocks by hash, get-block-range streams a contiguous chain of blocks identified by a single anchor — typically a hash the requester has verified against a trusted commitment, a validated header, or an already-verified block (see 13).

The responder streams blocks until it has delivered count blocks, or until delivering the next block would bring the total serialized size of the delivered blocks above max_bytes, whichever comes first — except that it MUST be willing to deliver the first block regardless of max_bytes, so that a maximum-size block is always retrievable. A response that exceeds either bound — more than count blocks, or any block after the first whose delivery brings the total serialized size above max_bytes — is a connection error of type FLOOD; both bounds are exact, so an honest responder never exceeds them. Because block sizes vary by three orders of magnitude across the chain's history, max_bytes — not count — is what bounds the work a stream commits one peer to; scheduling recommendations are given in 13.

Delivery is in descending height order so that every block is verifiable on arrival: the first delivered block's header MUST hash to final_hash, each subsequent delivered block's header MUST hash to the hashPrevBlock of the previously delivered block, and each delivered block's transactions MUST match its header's transaction merkle root, and each delivered block's serialized size MUST NOT exceed the maximum block size permitted by the consensus rules. A delivered block that violates these rules is a connection error of type PROTOCOL_ERROR — the rules are checkable by hashing alone, so a violation is never attributable to a different chain view. A requester with a trusted anchor therefore needs no download handles for the range's interior blocks and can assign blame exactly.

These checks bind each delivered block to the committed chain, but they do not bind all of its bytes. The transaction merkle root commits to txids, and from NU5 a v5 transaction's txid excludes its authorizing data — scriptSigs, proofs, and signatures — which is committed separately by hashAuthDataRoot within the header's hashBlockCommitments (see ZIP 244 28). Recovering the authorizing data commitment from the header requires the chain history root at the parent block, which arrival-time checking does not have, so this check is necessarily deferred to validation time. A responder can consequently deliver blocks that satisfy every rule above while carrying arbitrary authorizing data, padded up to the element size limit.

A requester therefore MUST NOT store a delivered block, serve it to another peer, or use it to regenerate a synchronization artifact until its authorizing data commitment has been checked against its header. A block whose authorizing data commitment fails that check SHOULD incur a misbehavior penalty of 100 points against the peer that delivered it: the delivered block matches the committed chain by txid, so the discrepancy is attributable to that peer and not to a different chain view. A requester SHOULD also bound the bytes it accepts on a stream by max_bytes, independently of the block count, since a block may be padded well beyond the size its hints predicted.

The responder serves the ancestor chain of the block with hash final_hash, whether or not that block is in its best chain. It MAY also finish its sending direction early, before either request bound is reached (for example, to bound the resources committed to one stream). Truncation for any reason is resumable, since the requester knows the next expected hash (the hashPrevBlock of the last delivered block) and MAY re-request the remainder — from the same or a different peer — using it as final_hash; a stream truncated by max_bytes thereby self-chunks a long span into byte-bounded, individually verified units even when the requester has no per-block size information. A requester that no longer wants the remainder cancels the responder's sending direction with CANCELLED; delivered blocks remain verified and usable. Transport flow control bounds the buffering on both sides.

get-tree-roots

Stream type: 0x08

Request:

Size Field Description
4 start_height Height of the first requested entry (uint32, little-endian).
32 final_hash Hash of the block at the highest requested height, start_height + count − 1 (see below).
4 count Maximum number of entries requested (uint32, little-endian). MUST NOT exceed 4,000.

Response:

Size Field Description
varies count Number of entries (CompactSize).
varies entries count entries, each encoded as follows.

Entry k, for k from 0 to count − 1, describes the block at height start_height + k of the chain identified by final_hash — the block with hash final_hash and its ancestors:

Size Field Description
32 sapling_root Root of the Sapling note commitment tree after this block.
32 orchard_root Root of the Orchard note commitment tree after this block.
32 ironwood_root Root of the Ironwood note commitment tree after this block.
varies sapling_txs Number of transactions in this block with Sapling components, as counted by the block's chain history tree leaf (CompactSize; see ZIP 221 29).
varies orchard_txs Number of transactions in this block with Orchard-pool components (CompactSize).
varies ironwood_txs Number of transactions in this block with Ironwood-pool components (CompactSize).
32 auth_data_root The block's authorizing data commitment hashAuthDataRoot (see ZIP 244 28).

For a pool that is not active at the entry's height, the corresponding root field is 32 zero bytes and the corresponding count is 0.

Requests the per-block data from which a synchronizing node can reproduce each block's note commitment tree roots and chain history tree without recomputing the trees from the blocks' note commitments — the dominant CPU cost of validating historical blocks under checkpoint-based synchronization.

The final_hash field anchors the request to a specific chain: the requester sets it to the hash — taken from headers it has already authenticated (see 13) — of the block at the highest requested height, and the requested entries describe that block and its ancestors, which final_hash identifies uniquely. A responder whose best chain does not contain the block with hash final_hash at height start_height + count − 1 MUST refuse the stream with REFUSED rather than serve entries for different blocks. An honest peer with a divergent chain view therefore refuses rather than serving mismatching entries, so an entry that fails verification is never attributable to a different chain view (cf. the tolerance rule of Checkpointed Synchronization, which this anchoring makes unnecessary here).

The entries are not self-authenticating: a node MUST NOT rely on any part of an entry for any purpose until it has verified that part against the chain's header commitments, as specified in 13. Which commitment applies depends on the height, and changes at Heartwood rather than at NU5: between Sapling and Heartwood activation the header field hashFinalSaplingRoot is the block's Sapling root and is compared directly; from Heartwood activation ZIP 221 29 repurposes that field to carry the chain history root (hashChainHistoryRoot, and from NU5 hashBlockCommitments, which binds it together with the authorizing data commitment of ZIP 244 28), and entries are verified by reconstructing the chain history tree.

Not every field of an entry is authenticated at every height. The chain history tree leaf defines no Ironwood field, so no header commits to an ironwood_root; and reconstruction verifies an entry only once the following block's header has been checked against it, so the highest entry of a response is not yet verified when the response completes. A node MUST NOT rely on an unverified root, and recomputes the corresponding tree instead.

An entry that fails verification against an authenticated header SHOULD incur a misbehavior penalty (see Misbehavior and Banning). A node MUST NOT assign that penalty on the basis of a check that does not apply at the entry's height: in particular, comparing a supplied Sapling root against hashFinalSaplingRoot above Heartwood activation compares it against an unrelated commitment and would penalize every honest responder.

Serving get-tree-roots requires a per-block root index that not every node maintains (in particular, a node that itself synchronized without computing historical trees may not have one). A node that maintains the index advertises the NODE_TREE_ROOTS service flag (see Service Flags). The response MAY contain fewer entries than requested, and MAY be empty; truncation MUST be a prefix (the returned entries MUST correspond to the lowest requested heights). A node without the index refuses the stream with REFUSED. The response count MUST NOT exceed the requested count.

get-object

Stream type: 0x09

Request:

Size Field Description
32 hash SHA-256 hash of the requested object.
8 offset Byte offset into the object at which to start (uint64, little-endian). MUST NOT exceed 2^48 − 1.
4 length Maximum number of bytes requested (uint32, little-endian). MUST NOT exceed 33,554,432 (32 MiB).

Response:

Size Field Description
1 result 0x00: the object is available; its size and data follow. 0x02: not found (nothing follows).
varies size Total size of the object in bytes (CompactSize).
varies data The object's bytes from offset, up to length bytes or the end of the object, whichever comes first.

Requests a byte range of a synchronization artifact: an immutable, content-addressed object identified by the SHA-256 hash of its contents. The protocol does not interpret artifact contents; artifacts are named by trusted commitments and synchronization metadata (for example, known-hash chunk files and state snapshot pieces; see 13), and a node serves whichever artifacts it holds or can reproduce deterministically from its chain state. A node that serves artifacts advertises the NODE_SYNC_ARTIFACTS service flag (see Service Flags).

The responder MAY finish its sending direction early, having delivered fewer than the requested bytes; the requester detects the shortfall from size and MAY re-request the remainder from any peer, since object bytes are position-addressed and identical everywhere. An object is verified by hashing its complete contents; piece sizing conventions that keep each piece independently fetchable and verifiable are specified in 13.

The number of bytes returned is determined as follows, and a responder MUST evaluate the two cases in this order:

  1. If offset is greater than or equal to the object's size, the response is result = 0x00, the object's size, and no data.
  2. Otherwise the responder returns at most min(length, size − offset) bytes, starting at offset.

Evaluating the bound as min(offset + length, size) is not equivalent: the sum can wrap, yielding an end position below offset and a byte count that underflows to an enormous value. offset is bounded above for the same reason, and a responder MUST reject a request whose offset exceeds that bound as a connection error of type PROTOCOL_ERROR.

A response carrying more than length bytes of data, or data extending beyond the end of the object as given by its own size field, is a connection error of type FLOOD.

The same discipline applies to the requester. A response's size is peer-supplied and unauthenticated until the assembled object is hashed: a requester MUST NOT compute a remaining length as size − received without first checking that size is at least received, and a peer reporting a size inconsistent with a previous response for the same hash, or below the bytes it has already delivered, is a connection error of type PROTOCOL_ERROR.

Object data is verified only by hashing the complete object, so misdelivery is attributable only when a single peer delivered every byte: a node that has fetched all of an object's bytes from one peer, and finds that they do not hash to the requested hash, SHOULD assign that peer a misbehavior penalty (see Misbehavior and Banning), so that a peer answering with well-formed garbage is deselected rather than reassigned the same work indefinitely. A node that assembled an object from ranges delivered by more than one peer MUST NOT assign a misbehavior penalty when the assembled object fails verification — any of the contributing peers could be at fault — and SHOULD instead discard the data and re-fetch the object with each candidate peer serving the whole object, which isolates a misbehaving peer. Disagreement between peers about an object's size is resolved by the hash check, not penalized.

Announcement Stream Types

Block Announcements

Stream type: 0x10

Each record payload on a block announcement stream has the following format:

Size Field Description
1 kind 0x00: header announcement. 0x01: compact block announcement.
varies payload For kind = 0x00: a serialized block header 12. For kind = 0x01: a compact block (see Compact Block Encoding).

Blocks are announced immediately; they are not subject to the trickling delay applied to transactions.

A node announces a new block to a given peer in one of two ways:

  1. If the peer requested high-bandwidth compact block announcements (the announce field of its init record was 1), by sending a compact block announcement (kind = 0x01).
  2. Otherwise, by sending header announcements (kind = 0x00) for the new block and for any intermediate blocks the peer has not yet been sent, up to a small implementation-defined limit. This follows the design of BIP 130 30.

A node MUST NOT send compact block announcements to a peer whose init record had announce = 0. Header announcements MAY be sent to any peer, regardless of its announce preference; in particular, if a block's compact block encoding would exceed the maximum record payload length (see Records), the sender announces that block with a header announcement instead.

A node SHOULD NOT send a header announcement for a block it has not fully validated, except as the oversize substitute above for a compact block announcement to a peer that requested high-bandwidth announcements. Correspondingly, the pre-validation penalty exemption of Relay Protocol applies to announcements of either kind received on a connection on which the receiving node requested high-bandwidth announcements (its init record had announce = 1); a header announcement received where the receiver's announce was 0 carries no such exemption, and an announced block that fails full validation is subject to the rules of Misbehavior and Banning.

An announcing node SHOULD only announce a block if it expects the peer to be able to connect the announced header to the peer's known chain (for example, because the parent block was previously announced to or by that peer). A node receiving an announcement whose header does not connect to its known chain SHOULD recover by sending a get-headers request, and MUST NOT assign a misbehavior penalty solely because a header does not connect.

Transaction Announcements

Stream type: 0x11

Each record payload on a transaction announcement stream is a single transaction reference (see Transaction References) of type TXID or WTXID. SHORTID references MUST NOT be used in announcements.

A node MUST NOT open a transaction announcement stream to a peer whose init record had relay = 0.

Announcements are subject to the trickling delay of Trickling. Interested peers fetch announced transactions with get-tx requests.

Address Announcements

Stream type: 0x12

Each record payload on an address announcement stream is a single network address record (see Network Address Record).

Address announcements are subject to the rate limits and address book management practices of Address Relay.

Draft arya-jvff-p2p-quic-transport: Version 2 Zcash P2P Network Protocol — Part 2 · ZIPs.tools