# Draft: Version 2 Zcash P2P Network Protocol

Status: Draft

Owners: Arya, Janito

Snapshot: a34cfc091434275120ff017c92c3b50d3cef623a

Source date: 2026-10-16T02:56:04+01:00

[Source revision](https://github.com/zcash/zips/commit/a34cfc091434275120ff017c92c3b50d3cef623a)

[Official source](https://zips.z.cash/draft-arya-jvff-p2p-quic-transport) · [Source file](https://github.com/zcash/zips/blob/a34cfc091434275120ff017c92c3b50d3cef623a/zips/draft-arya-jvff-p2p-quic-transport.md) · [Discussion](https://github.com/zcash/zips/issues/352)

## Document parts

[Part 1](https://zips.tools/draft/draft-arya-jvff-p2p-quic-transport) · [Part 2](https://zips.tools/draft/draft-arya-jvff-p2p-quic-transport?part=2) · [Part 3](https://zips.tools/draft/draft-arya-jvff-p2p-quic-transport?part=3)

Reading part 2 of 3. Follow all parts for the complete proposal.

| 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`](#get-tree-roots) requests. |
| `NODE_SYNC_ARTIFACTS` | 4 | The node serves content-addressed synchronization artifacts via [`get-object`](#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 <a id="user-content-fnref-draft-sync-4"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)).

Bits 24–31 remain reserved for temporary experiments, as in ZIP 204
<a id="user-content-fnref-zip-0204-serviceflags"></a>
[24](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-serviceflags).

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

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

<a id="connection-handshake"></a>

### 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](#streamtypes)); 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](#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](#compactblockrelay). |
| 1 | `full_ids` | Whether the sender requests full transaction IDs in compact block announcements (`uint8`; 0 or 1). See [Full Transaction IDs](#fulltransactionids). |

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](#protocolversioning)), and at least the
  protocol version associated with the current network epoch (see
  [Network Upgrade Epoch Enforcement](#networkupgradeepochenforcement)). 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](#initrecord).

- 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...
```

<a id="protocol-versioning"></a>

### 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](#networkupgradeepochenforcement)), 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 <a id="user-content-fnref-zip-0204-assignment"></a>
[25](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-assignment) 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 <a id="user-content-fnref-zip-0204-assignment-2"></a>
[25](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-assignment) assigns to the network upgrade
that deploys this protocol (see [Deployment](/draft/draft-arya-jvff-p2p-quic-transport?part=3#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 <a id="user-content-fnref-zip-0200-2"></a>
[3](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0200)), 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 <a id="user-content-fnref-zip-0204-epochs"></a>
[26](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-epochs).

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 <a id="user-content-fnref-zip-0201"></a>
[27](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0201).

<a id="request-stream-types"></a>

### 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](#requeststreams).

#### `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 <a id="user-content-fnref-protocol-blockheader-5"></a> [12](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockheader) (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 <a id="user-content-fnref-protocol-txnencoding-3"></a> [11](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-txnencoding). |
| 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](#fulltransactionids)) 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](#relayprotocol)). 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](#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](#compactblockencoding)). 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](#misbehaviorandbanning)); this is the
handling of such responses, in place of the connection error that
[Request Streams](#requeststreams) 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](#serializedblocks)). |

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](#compactblockrelay)).

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 <a id="user-content-fnref-draft-sync-5"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)).

#### `get-tx`

Stream type: `0x03`

**Request:**

| Size | Field | Description |
| --- | --- | --- |
| varies | `count` | Number of transaction references (CompactSize). |
| varies | `txrefs` | `count` transaction references (see [Transaction References](#transactionreferences)). 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 <a id="user-content-fnref-protocol-txnencoding-4"></a> [11](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-txnencoding). |

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](#requestingmissingtransactions).

#### `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](#networkaddressrecord)). |

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](#misbehaviorandbanning)); this is the handling of
such responses, in place of the connection error that
[Request Streams](#requeststreams) would otherwise prescribe.

Each returned record carries the address's last-known service flags in its
`services` field (see [Service Flags](#serviceflags) and
[Network Address Record](#networkaddressrecord)). 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](#records)), each a single transaction reference (see
[Transaction References](#transactionreferences)) 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](#requeststreams)): 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](#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](#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](#serializedblocks)) (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 <a id="user-content-fnref-draft-sync-6"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)) 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 <a id="user-content-fnref-draft-sync-7"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)). Cumulative
`txouts` values additionally locate each block's bits within the
spentness-hint bitmap of <a id="user-content-fnref-draft-sync-8"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync). 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](#checkpointedsynchronization)). 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](#checkpointedsynchronization). 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](#misbehaviorandbanning))
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`](#get-block-range) and
ZIP 244 <a id="user-content-fnref-zip-0244"></a>
[28](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0244)). 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](#serializedblocks)), 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 <a id="user-content-fnref-draft-sync-9"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)).

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 <a id="user-content-fnref-draft-sync-10"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync).

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* —
`scriptSig`s, proofs, and signatures — which is committed separately by
`hashAuthDataRoot` within the header's `hashBlockCommitments` (see ZIP 244
<a id="user-content-fnref-zip-0244-2"></a>
[28](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0244)). 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 <a id="user-content-fnref-zip-0221"></a> [29](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0221)). |
| 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 <a id="user-content-fnref-zip-0244-3"></a> [28](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0244)). |

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 <a id="user-content-fnref-draft-sync-11"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)) — 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](#checkpointedsynchronization), 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 <a id="user-content-fnref-draft-sync-12"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync).
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 <a id="user-content-fnref-zip-0221-2"></a>
[29](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0221) 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 <a id="user-content-fnref-zip-0244-4"></a>
[28](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0244)), 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](#misbehaviorandbanning)). 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](#serviceflags)). 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 <a id="user-content-fnref-draft-sync-13"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)), 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](#serviceflags)).

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
<a id="user-content-fnref-draft-sync-14"></a>
[13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync).

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](#misbehaviorandbanning)), 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.

<a id="announcement-stream-types"></a>

### 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 <a id="user-content-fnref-protocol-blockheader-6"></a> [12](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockheader). For `kind = 0x01`: a compact block (see [Compact Block Encoding](#compactblockencoding)). |

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
   <a id="user-content-fnref-bip-0130"></a>
   [30](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-bip-0130).

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](#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](#relayprotocol) 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](#misbehaviorandbanning).

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](#transactionreferences))
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](#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](#networkaddressrecord)).

Address announcements are subject to the rate limits and address book
management practices of [Address Relay](#addressrelay).

## Citation relationships

Cites: [ZIP 155](https://zips.tools/zip/155), [ZIP 200](https://zips.tools/zip/200), [ZIP 201](https://zips.tools/zip/201), [ZIP 204](https://zips.tools/zip/204), [ZIP 221](https://zips.tools/zip/221), [ZIP 239](https://zips.tools/zip/239), [ZIP 244](https://zips.tools/zip/244)

Cited by: None