ZIP: XXX
Title: Version 2 Zcash P2P Network Protocol
Owners: Arya <[email protected]>
Janito <[email protected]>
Status: Draft
Category: Network
Created: 2026-07-31
License: MIT
Discussions-To: <https://github.com/zcash/zips/issues/352>
The key words “MUST”, “MUST NOT”, “SHOULD”, “SHOULD NOT”, “MAY”, and “RECOMMENDED” in this document are to be interpreted as described in BCP 14 1 when, and only when, they appear in all capitals.
The terms “Mainnet” and “Testnet” are to be interpreted as described in section 3.12 of the Zcash Protocol Specification. 2
The term “network upgrade” is to be interpreted as described in ZIP 200. 3
The term “block chain” in this document is to be interpreted as described in section 3.3 of the Zcash Protocol Specification. 4
The term “legacy protocol” in this document refers to the Zcash P2P network protocol specified in ZIP 204 5.
This ZIP specifies version 2 of the Zcash peer-to-peer network protocol, a successor to the protocol specified in ZIP 204 5. It specifies the protocol’s transport-neutral stream layer, the QUIC and Tor transports that realize it, the connection handshake, stream and record types, and relay behavior, and is intended to be sufficient for an implementor to build a conformant Zcash network peer.
ZIP 204 5 specifies the current Zcash P2P network protocol, whose message-passing architecture is inherited from Bitcoin: a single unencrypted TCP bytestream carrying framed messages with implicit correlation between requests and responses. This ZIP proposes a successor protocol that replaces that architecture with multiplexed streams carried over an arbitrary transport that provides a secure channel between peers and a means of identifying the network and addressing peers. The protocol is specified against that abstract transport interface; this ZIP defines two concrete transports, one based on QUIC 6 and one carried over Tor onion services 7. The motivations for this change are:
Transport security. The legacy transport was unencrypted and unauthenticated, exposing all P2P traffic to passive network observers. Every transport of this protocol provides an encrypted channel: the QUIC transport integrates TLS 1.3 8 9, and the Tor transport inherits the encryption of onion service connections. (Bitcoin addressed the same gap with a bespoke encrypted transport, BIP 324 10; this protocol provides equivalent protection using standardized protocols.)
Multiplexing without head-of-line blocking. On the legacy transport, a
single 2 MiB block message stalled all other traffic on the connection.
Streams allow concurrent block downloads, transaction relay, and control
traffic to proceed independently — on the QUIC transport, at both the
application and packet-loss level.
Explicit request correlation. Legacy responses (block, tx, headers,
notfound) carried no request identifiers, forcing implementations to
correlate them heuristically. In this protocol, each request occupies its own
stream, so correlation, cancellation, and per-request timeouts are
structural.
Simplification. Transport-level mechanisms that the legacy protocol carried in the application layer — payload checksums, network magic, keep-alives and latency measurement, flow control — are provided by the transport and are deleted from the application protocol.
No backwards compatibility with the legacy protocol is provided; this ZIP replaces it entirely. See Deployment.
This ZIP does not specify the encoding of transactions and block headers; for those, refer to the Zcash Protocol Specification 11 12. The encoding of a whole block in terms of its header and transactions is part of the peer-to-peer protocol rather than the consensus protocol 12, and is specified in Serialized Blocks.
This section is non-normative. Relative to the legacy protocol specified in ZIP 204 5, this ZIP:
SHA-256d checksum fields are
subsumed by QUIC framing and authenticated encryption.version/verack handshake with a single init record
exchange on a dedicated handshake stream (see
Connection Handshake). The timestamp, addr_recv,
and addr_from fields are removed.ping and pong; QUIC provides keep-alives and RTT measurement.reject; QUIC application error codes are used instead (see
Application Error Codes).inv, getdata, and notfound; announcements are carried on
dedicated unidirectional streams, and object requests carry per-item results.getblocks message; headers-first
synchronization is the baseline synchronization method, and new request
streams support checkpoint-based bulk synchronization: get-hashes
(known hashes and sync-cost metadata), get-block-range (anchored block
streaming), get-tree-roots (note commitment tree roots, verified
against header commitments), and get-object (content-addressed
synchronization artifacts). See Synchronization and
13.alert message.filterload, filteradd, filterclear)
and the NODE_BLOOM service flag.addr message and CAddress/CService encodings; the
addrv2 address encoding of ZIP 155 14 is the only address format.get-headers responses (see Block Relay).This specification is organized bottom-up. Transports defines how connections are established and secured (QUIC and Tor); Stream Layer defines the typed streams that carry all application data, and Data Types and Encoding the shared encodings. Connection Handshake and Protocol Versioning cover connection setup. Request Stream Types and Announcement Stream Types specify the individual request and announcement formats, and Block Relay, Synchronization, Transaction Relay, and Address Relay the behaviors built on them.
There are three Zcash networks: Mainnet, Testnet, and Regtest. Every connection is bound to exactly one network, determined during connection establishment as each transport defines (see Transports); this replaces the network magic bytes of the legacy protocol.
The DNS seed hostnames used for initial peer discovery on each network are those listed in ZIP 204 15. Regtest does not use DNS seeds or hardcoded seed nodes.
A node discovers peers through the following mechanisms:
Persisted addresses. A node SHOULD persist the peer addresses it learns (see Address Relay) across restarts, and on startup SHOULD attempt connections to previously learned addresses before consulting seeds.
DNS seeding. When the node has no usable persisted addresses — on first start, or when too few persisted addresses yield successful connections — it queries the DNS seed hostnames listed above for A and AAAA records, and attempts connections to the resulting addresses on the default port of the QUIC transport.
Hardcoded seed nodes. If DNS seeding fails or is insufficient, the node MAY fall back to a compiled-in list of seed node addresses. DNS seeding is preferred over hardcoded seed nodes.
Address relay. Connected peers exchange address information using
get-addr request streams and address announcement streams (see
get-addr and
Address Announcements). This mechanism operates
independently of the above.
DNS seeding yields IP addresses only, and so bootstraps only IP-based
transports such as QUIC. Addresses of peers reachable over overlay transports —
such as the TORV3 addresses of the Tor transport (see
Tor Transport) — are learned through address relay or provided
by local configuration.
The protocol semantics in this document are defined against the abstract stream layer of Stream Layer. A transport is a concrete mechanism that carries connections between peers and realizes that stream layer. This ZIP defines two transports: one based on QUIC (see QUIC Transport), and one carried over Tor onion services (see Tor Transport). Future revisions may define additional transports — a transport carrying the stream layer over the Nym mixnet is anticipated (see Deployment) — and a node MAY support several transports simultaneously. The protocol semantics on a connection are identical regardless of its transport.
Every transport MUST provide:
The transport provides keep-alives and round-trip time measurement; the legacy
ping/pong messages have no equivalent in this protocol.
The specific connection limits are implementation-defined; the legacy reference values in ZIP 204 5 remain reasonable defaults.
A node SHOULD maintain at most one connection to a given remote address. Self-connection detection via the handshake nonce is specified in Init Record.
The QUIC transport uses QUIC version 1 6 over UDP, secured with TLS 1.3 8 as specified by RFC 9001 9.
PROTOCOL_ERROR error code.
(0-RTT data is replayable by an attacker.)max_datagram_frame_size transport parameter and MUST NOT send
DATAGRAM frames.max_idle_timeout transport parameter consistent with the idle timeout of
Connection Management.The network of a QUIC connection is identified by an Application-Layer Protocol Negotiation (ALPN) 17 protocol identifier, negotiated in the QUIC-TLS handshake:
| Network | ALPN Identifier |
|---|---|
| Mainnet | zcash/main |
| Testnet | zcash/test |
| Regtest | zcash/regtest |
A node MUST offer exactly the ALPN identifier of the network it operates on, and MUST NOT complete a connection on which ALPN negotiation did not select that identifier.
Incompatible future revisions of this transport will be assigned new ALPN identifiers.
The QUIC transport uses the default port numbers of the legacy protocol, as specified in ZIP 204 18, as UDP port numbers.
Connections are encrypted but endpoints are not authenticated: the goal is protection against passive network observers, not endpoint identity.
Bulk synchronization moves hundreds of gigabytes over the request streams of this protocol, and its throughput is governed by transport configuration rather than by the protocol itself. The following applies to nodes that serve or perform bulk synchronization (see 13):
The abstract stream layer of Transport Requirements maps onto QUIC as follows:
| Stream layer concept | QUIC realization |
|---|---|
| Bidirectional / unidirectional stream | QUIC bidirectional / unidirectional streams. |
| Finishing a stream direction | A STREAM frame with the FIN bit. |
| Resetting a stream with an error code | RESET_STREAM carrying the application error code. |
| Cancelling a peer’s sending with an error code | STOP_SENDING carrying the application error code. |
| Closing the connection with an error code | CONNECTION_CLOSE (application variant) carrying the application error code. |
| Per-stream backpressure | QUIC stream and connection flow control. |
| Stream concurrency limits | initial_max_streams_bidi, initial_max_streams_uni, and MAX_STREAMS frames. |
| Keep-alive | QUIC PING frames. |
The Tor transport carries connections over the Tor network 20 as connections to version 3 onion services 7. It serves traffic for which the metadata protection of onion routing outweighs its latency cost (see Deployment and Security and Privacy Considerations), and allows a node to participate in the network without exposing an IP address.
TORV3 network address record
(see Network Address Record); the record’s port
field is the onion service virtual port, which SHOULD be the default port of
the network (see Default Ports).TORV3 address encodes). TLS is not
used inside the connection, and the initiator is deliberately not
authenticated (cf. Certificates).Note: The underlying Tor connection is a single TCP-like bytestream, so loss or congestion on it stalls all streams of the connection; the framing layer removes head-of-line blocking between streams at the application level only (see Deployment for transport selection).
Immediately after the connection is established, each peer sends a preamble, before any frames:
| Size | Field | Description |
|---|---|---|
| varies | network |
Network identifier string (CompactSize-prefixed; see below). |
| varies | initial_max_data |
Initial connection-level flow control credit in bytes (CompactSize). |
| varies | initial_max_stream_data |
Initial per-stream flow control credit in bytes (CompactSize). |
| varies | initial_max_streams_bidi |
Initial limit on the peer’s concurrent bidirectional streams (CompactSize). |
| varies | initial_max_streams_uni |
Initial limit on the peer’s concurrent unidirectional streams (CompactSize). |
The network string is the identifier of the node’s network, using the same
values as the QUIC transport’s ALPN identifiers (see
Network Identification): zcash/main, zcash/test,
or zcash/regtest. A node MUST close the connection if the peer’s network
string differs from its own; this replaces ALPN-based network identification,
which is unavailable without TLS.
The flow control fields play the role of the corresponding QUIC transport
parameters: they are the initial credit available to the peer for the
corresponding limit (see Stream Framing). A node MUST allow
an initial_max_stream_data of at least 2,228,224 bytes (the maximum record
payload length plus framing headroom), and SHOULD allow the stream concurrency
minimums of Transport Requirements.
The preamble precedes every frame, and therefore precedes the record payload limit of Records and the malformed-frame rule of Stream Framing; neither bounds it. Its fields are bounded here instead, and a node MUST enforce these bounds while parsing the preamble, before allocating storage for a field:
network string MUST NOT exceed 16 bytes.The handshake timeout of
Connection Management runs from the establishment of
the transport connection, so it covers the preamble as well as the init
record; an initiator that opens a connection and stalls mid-preamble is closed
by it. This matters most for this transport: onion service addresses are free
to generate and effectively unbannable (see
Misbehavior and Banning), so a connection slot held
open at no cost is the resource an attacker is actually consuming.
Incompatible future revisions of this transport will be assigned new network identifier strings.
The stream layer is realized over the connection’s ordered bytestream by a framing layer whose stream semantics — stream states, flow control, and error signalling — are modeled on those of QUIC 6.
Stream IDs are integers. The two least significant bits of a stream ID encode
its kind, as in QUIC: 0x0 initiator-opened bidirectional, 0x1
responder-opened bidirectional, 0x2 initiator-opened unidirectional, 0x3
responder-opened unidirectional. Successive streams of each kind are opened
with sequentially increasing stream IDs; a stream is opened implicitly by the
first frame that its opener sends referencing its ID.
Each frame is a 1-byte frame type followed by type-dependent fields. Integer fields, including stream IDs, are CompactSize-encoded.
| Type | Frame | Fields | Semantics |
|---|---|---|---|
0x00 |
PING |
8-byte opaque value | Keep-alive and RTT measurement; the receiver MUST respond with PONG. |
0x01 |
PONG |
8-byte echoed value | Response to PING. |
0x02 |
STREAM |
stream ID, flags (1 byte), length, data | Appends data to the stream. Flag bit 0 (FIN) finishes the sender’s direction of the stream. |
0x03 |
RESET_STREAM |
stream ID, error code | Resets the sender’s direction of the stream (see Transport Requirements). |
0x04 |
STOP_SENDING |
stream ID, error code | Requests that the peer stop sending on the stream. |
0x05 |
MAX_DATA |
maximum | Raises the connection-level flow control limit (cumulative bytes of stream data). |
0x06 |
MAX_STREAM_DATA |
stream ID, maximum | Raises the flow control limit of one stream (cumulative bytes). |
0x07 |
MAX_STREAMS_BIDI |
count | Raises the limit on the peer’s cumulative count of opened bidirectional streams. |
0x08 |
MAX_STREAMS_UNI |
count | Raises the limit on the peer’s cumulative count of opened unidirectional streams. |
0x09 |
CLOSE |
error code | Closes the connection with an application error code; the sender then closes the underlying connection. |
Flow control limits are cumulative, as in QUIC: a limit counts total bytes
sent on a stream (respectively, on all streams; respectively, total streams
opened) since the beginning of the connection, and each MAX_* frame
communicates a new absolute limit. Limits never decrease; a MAX_* frame with
a lower value than a previously communicated limit is ignored. A node MUST NOT
exceed a limit communicated by its peer; a peer that does so is a connection
error of type PROTOCOL_ERROR (equally, a frame that is malformed or has an
unrecognized frame type).
The length of a STREAM frame MUST NOT exceed 65,536 bytes; larger
application writes are split across multiple STREAM frames, allowing frames
of other streams to be interleaved between them.
The abstract stream layer maps onto this framing as follows:
| Stream layer concept | Framing realization |
|---|---|
| Bidirectional / unidirectional stream | Stream ID kinds 0x0/0x1 and 0x2/0x3. |
| Finishing a stream direction | A STREAM frame with the FIN flag. |
| Resetting a stream with an error code | RESET_STREAM carrying the application error code. |
| Cancelling a peer’s sending with an error code | STOP_SENDING carrying the application error code. |
| Closing the connection with an error code | CLOSE carrying the application error code. |
| Per-stream backpressure | MAX_DATA and MAX_STREAM_DATA credit. |
| Stream concurrency limits | Preamble initial limits and MAX_STREAMS_* frames. |
| Keep-alive | PING frames. |
All application data is carried on streams provided by the transport. The first byte of every stream is a stream type that determines the format and semantics of the remaining stream data.
The stream layer that every transport provides consists of bidirectional streams (both peers can send) and unidirectional streams (only the opener can send), with the following operations and guarantees:
How each transport realizes these operations is specified in the corresponding transport section (for QUIC, see Stream Layer Mapping).
| Code | Name | Kind | Description |
|---|---|---|---|
0x00 |
Handshake | Bidirectional, initiator only | Connection handshake and control (see Connection Handshake). |
0x01 |
get-headers |
Bidirectional | Request block headers, optionally with transaction IDs. |
0x02 |
get-blocks |
Bidirectional | Request full blocks. |
0x03 |
get-tx |
Bidirectional | Request transactions. |
0x04 |
get-addr |
Bidirectional | Request peer addresses. |
0x05 |
get-mempool |
Bidirectional | Subscribe to mempool contents. |
0x06 |
get-hashes |
Bidirectional | Request best-chain block hashes and per-block sync-cost metadata. |
0x07 |
get-block-range |
Bidirectional | Stream a contiguous range of blocks, verified against an anchor hash. |
0x08 |
get-tree-roots |
Bidirectional | Request per-block note commitment tree roots for a height range. |
0x09 |
get-object |
Bidirectional | Request a range of a content-addressed synchronization artifact. |
0x10 |
Block announcements | Unidirectional | Announce new blocks (see Block Announcements). |
0x11 |
Transaction announcements | Unidirectional | Announce new transactions (see Transaction Announcements). |
0x12 |
Address announcements | Unidirectional | Gossip peer addresses (see Address Announcements). |
Stream type 0x00 and the ranges 0x01–0x0F and 0x10–0x1F are reserved
for the handshake, request stream types, and announcement stream types
respectively; future revisions are expected to assign new stream types from the
matching range.
To refuse a stream opened by its peer, a node cancels the peer’s sending direction of that stream with the applicable error code and, if the stream is bidirectional, also resets its own sending direction with the same code; refusing a unidirectional stream involves only the cancellation.
A node receiving a stream whose type byte it does not recognize MUST refuse
that stream with the UNSUPPORTED_STREAM_TYPE error code (see
Application Error Codes) and MUST NOT treat it as a
connection error or assign a misbehavior penalty. This allows future stream
types to be deployed without version gating.
A stream that is finished by its opener before a complete type byte has been
received is a connection error of type PROTOCOL_ERROR.
A request stream is a bidirectional stream carrying exactly one request and its response:
get-mempool.)Either peer may open request streams; there is no client/server asymmetry beyond the rules stated for individual stream types.
REFUSED error code instead of sending a
response.CANCELLED error code; the responder SHOULD then
reset the stream and abandon work on the request.PROTOCOL_ERROR.result value in a response — is a
connection error of type PROTOCOL_ERROR, unless other handling is
specified for the stream type.A node bounds the number of concurrent requests a peer may have outstanding using the transport’s stream concurrency limits (see Transport Requirements).
An announcement stream is a long-lived unidirectional stream carrying a sequence of records for one announcement topic.
PROTOCOL_ERROR. If an announcement stream is
reset or finished, the sender MAY open a replacement.PROTOCOL_ERROR, unless other handling is specified for
the stream type. (New record formats are introduced under new protocol
versions, so an unrecognized format is a violation, not a newer peer.)Each record on an announcement or handshake stream is encoded as a fixed-size 4-byte length prefix followed by that many bytes of payload:
| Size | Field | Description |
|---|---|---|
| 4 | length |
Payload length in bytes (uint32, little-endian). |
| varies | payload |
Record payload; format depends on the stream type. |
The maximum length of a record payload — and of any individually
length-prefixed element in a request or response, such as a serialized block —
is 2,097,152 bytes (2 MiB). A node MUST treat a length prefix exceeding this
limit as a connection error of type FLOOD.
A record MUST NOT be processed until it is complete. A stream that is finished
in the middle of a record is a connection error of type PROTOCOL_ERROR.
The following application error codes are used when closing a connection, resetting a stream, or cancelling a peer’s sending direction (see Transport Requirements):
| Code | Constant | Usage |
|---|---|---|
0x00 |
NO_ERROR |
Graceful connection close. |
0x01 |
PROTOCOL_ERROR |
The peer violated this specification. |
0x02 |
UNSUPPORTED_STREAM_TYPE |
Stream refusal: unrecognized stream type. |
0x03 |
OBSOLETE |
The peer’s protocol version is below the minimum for the current network epoch (see Network Upgrade Epoch Enforcement). |
0x04 |
SELF_CONNECTION |
The connection is to the node itself (see Init Record). |
0x05 |
FLOOD |
The peer exceeded a size or rate limit. |
0x06 |
MISBEHAVIOR |
The peer’s misbehavior score reached the ban threshold (see Misbehavior and Banning). |
0x07 |
CANCELLED |
Stop-sending/reset: the request is no longer wanted. |
0x08 |
REFUSED |
Stream reset: the responder declines to serve this request. |
0x09 |
INTERNAL_ERROR |
The sender encountered an internal error. |
Where this document states that an event is a connection error of a given type, a node detecting that event MUST close the connection with the corresponding error code.
A node receiving an error code it does not recognize MUST treat it as
INTERNAL_ERROR.
The integer types and byte order, the CompactSize variable-length integer encoding (including its canonicity requirement), and the string encoding are as specified in ZIP 204 21. As there, a node MUST reject non-canonical CompactSize encodings.
Every CompactSize count and length in this protocol is peer-supplied and may be as large as 264 − 1 on the wire, whatever bound this document places on its legitimate values. A node MUST check a count or length against its specified bound before using it, and MUST NOT allocate storage in proportion to a declared count before the corresponding data has been received. Where this document requires a bound on a sum of two counts, the node MUST also bound each addend, since a sum of two unbounded values can wrap and satisfy the bound on the sum. Likewise, a node computing an offset or a remaining length from peer-supplied values MUST perform the arithmetic in a way that cannot overflow or underflow.
The Zcash Protocol Specification defines the encodings of block headers and
transactions, but leaves the encoding of a whole block to the peer-to-peer
protocol 12. A serialized block is encoded as follows,
unchanged from the payload of the legacy block message:
| Size | Field | Description |
|---|---|---|
| varies | header |
The serialized block header 12 (including the Equihash solution). |
| varies | tx_count |
Number of transactions in the block (CompactSize). |
| varies | txns |
tx_count serialized transactions 11, in block order. |
Peer addresses use the addrv2 address record specified in ZIP 155
14; it is the only address encoding in this protocol. The record
fields (time, services, networkID, sizeAddr, addr, port), the
network IDs (IPV4, IPV6, TORV3, I2P, CJDNS), the 512-byte addr
limit, the per-network-ID length and encoding rules, and the prohibition on
gossiping addresses with unrecognized network IDs are all as specified in
ZIP 155.
The port field is interpreted per transport: for QUIC endpoints it is the
UDP port, and for TORV3 addresses it is the onion service virtual port (see
Tor Transport). It MUST be 0 if not relevant for the
network.
The time field of a relayed record is attacker-suppliable and
unauthenticated: it is the relaying peer’s claim about when the address was
last seen, and nothing binds it to reality. Because it is the freshness
signal an address manager ranks on, an unvalidated time set far in the
future makes an attacker’s addresses permanently the freshest entries of
whatever buckets they occupy, and so a preferred choice at every outbound
peer selection — an eclipse vector 22 that operates from inside the
buckets, where the bucketing of
Address Book Management affords no protection. A
node therefore:
time of a relayed record before storing it: a time more
than 10 minutes in the future, or equal to 0, is replaced by the current
time less 5 days.time is more than 30 days in the past.time the primary key of outbound peer
selection, and SHOULD prefer its own record of successful connections to
an address over any peer’s claim about it.These are the protections legacy implementations applied in code; they are stated here because the field is load-bearing for peer selection.
Note: Each network ID implies the transport by which the address is
reachable: IPV4, IPV6, and CJDNS addresses identify QUIC endpoints, and
TORV3 addresses identify onion services of the Tor transport. I2P
addresses are not reachable via either transport defined in this ZIP; they
remain defined for gossip so that nodes can learn them ahead of a future
revision that defines an I2P-capable transport.
Transactions are identified in announcements and requests by transaction
references. Per ZIP 239 23, transactions with version ≥ 5 are relayed
by wtxid (the txid followed by the authorizing data commitment auth_digest),
and transactions with version ≤ 4 by txid. A transaction reference is encoded
as a 1-byte type followed by a type-dependent identifier:
| Type | Name | Total Size | Description |
|---|---|---|---|
0x01 |
TXID |
33 bytes | Transaction with version ≤ 4, identified by its 32-byte txid. |
0x02 |
WTXID |
65 bytes | Transaction with version ≥ 5, identified by its 64-byte wtxid (txid followed by auth_digest). See ZIP 239 23. |
0x03 |
SHORTID |
39 bytes | Transaction within a specific block, identified by a 32-byte block hash followed by a 6-byte short transaction ID (see Short Transaction IDs). Only valid in get-tx requests. |
A transaction reference with an unrecognized type is a connection error of
type PROTOCOL_ERROR. Using the wrong reference type for a transaction’s
version is subject to a misbehavior penalty only where the sender of the
reference necessarily holds the transaction and so knows its version:
in announcement stream records and get-mempool response records (see
Misbehavior and Banning). A get-tx requester,
by contrast, may not hold the transaction it is requesting, and can have
derived the reference type from data supplied by another peer (see
Requesting Missing Transactions); a
responder receiving a get-tx reference whose type does not match the
referenced transaction’s version MUST answer that reference with a
not-found result, and MUST NOT assign a misbehavior penalty for it.
Service flags are advertised in the services field of init records and
network address records. Bit numbering is as in ZIP 204
| 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.
Peers perform an application handshake immediately after the transport connection is established.
0x00 (the
handshake stream) and sends an init record.init record on the same stream.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.
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.
A receiving node MUST validate the init record as follows:
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.nonce is checked for self-connection as specified in
Init Record.user_agent string MUST NOT exceed 256 bytes.relay, announce, and full_ids fields MUST each be 0 or 1.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.
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 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.
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.
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:
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.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 248 − 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:
offset is greater than or equal to the object’s size, the
response is result = 0x00, the object’s size, and no data.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.
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:
announce field of its init record was 1), by sending a compact block
announcement (kind = 0x01).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.
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.
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.
This protocol divides block headers and block transactions, rather than
transferring monolithic block messages as the legacy protocol did:
get-tx requests.Both mechanisms are unconditional; the per-connection preferences are carried
in the announce and full_ids fields of the init record (see
Init Record).
Compact block relay is adapted from BIP 152 31. This section specifies the protocol flows and the Zcash-specific differences from BIP 152; BIP 152 provides the design rationale.
The differences from BIP 152 are:
sendcmpct message. Compact block relay is unconditional, and
the high-bandwidth announcement preference is carried in the announce
field of the init record (see Init Record). (A future
change to the compact block encoding would be deployed under a new protocol
version.)getdata with
MSG_CMPCT_BLOCK has no equivalent, and compact blocks occur only as
high-bandwidth announcements. A peer announced to in low-bandwidth mode
instead requests the block’s coinbase transaction and transaction IDs via
get-headers with tx_ids = 1 (see Relay Protocol).full_ids field of the init record; see
Full Transaction IDs). BIP 152 supports only short
transaction IDs.getblocktxn and blocktxn messages. Missing transactions are
requested with get-tx requests (using SHORTID, TXID, or WTXID
references) and delivered in the corresponding responses; see
Requesting Missing Transactions.header field of a compact block uses the Zcash block header encoding
12, including the Equihash solution, rather than the
80-byte Bitcoin header.A compact block relays a block as its header plus a compact representation of its transactions:
| Size | Field | Description |
|---|---|---|
| varies | header |
The block header, encoded as a CompactSize length prefix followed by the serialized header 12 (including the Equihash solution). |
| 8 | nonce |
Nonce for short transaction ID computation (uint64, little-endian). If ids_kind is 1, this field SHOULD be 0 and MUST be ignored. |
| 1 | ids_kind |
0 if ids contains short transaction IDs; 1 if it contains full transaction IDs (uint8). |
| varies | ids_count |
Number of transaction IDs (CompactSize). |
| varies | ids |
If ids_kind is 0: short transaction IDs, 6 bytes each, little-endian (see Short Transaction IDs). If ids_kind is 1: full transaction IDs, 64 bytes each (see Full Transaction IDs). |
| varies | prefilled_count |
Number of prefilled transactions (CompactSize). |
| varies | prefilled_txns |
Prefilled transactions (see below). |
Each transaction in the block, in block order, is represented either by a
transaction ID in ids (short or full, according to ids_kind) or by a full
serialized transaction in prefilled_txns; the total number of transactions
in the block is ids_count + prefilled_count. Each of ids_count and
prefilled_count MUST NOT exceed 65,536, and their sum MUST NOT exceed
65,536; a compact block violating any of these is a connection error of type
FLOOD. Bounding the two counts individually is required as well as
bounding the sum: each is an independent CompactSize that may be as large as
264 − 1 on the wire, and a sum of two such values can wrap to a small
number that satisfies a bound the addends do not.
A node MUST set ids_kind to 1 in compact blocks sent to a peer that set
full_ids = 1 in its init record, and SHOULD set ids_kind to 0 otherwise.
A compact block with an ids_kind value other than 0 or 1 is a
nonconforming record (see Announcement Streams).
Each entry of prefilled_txns has the following format:
| Size | Field | Description |
|---|---|---|
| varies | index |
Differentially encoded index of this transaction within the block (CompactSize; see below). |
| varies | tx |
A full serialized transaction, encoded as a CompactSize length prefix followed by the serialized transaction 11. |
Prefilled transaction indexes are differentially encoded as in BIP 152
31. A compact block in which any absolute index is
greater than or equal to ids_count + prefilled_count, or in which indexes
overflow or are not strictly increasing, is a nonconforming record (see
Announcement Streams). The bound is the block’s own
transaction count, not the protocol maximum: an index below 65,536 can
still lie outside a small block, and a reconstructor sized to the block’s
actual transaction count would then write out of bounds.
The coinbase transaction MUST be prefilled. A sender SHOULD additionally prefill any transaction that it predicts the receiver does not have.
The short transaction ID of a transaction, relative to a given compact block, is computed as specified in BIP 152 31, with two substitutions:
auth_digest);
for a transaction with version ≤ 4, its 32-byte txid.nonce field to derive the SipHash
keys is the serialized Zcash block header 12 (as it
appears in the header field, without the length prefix).The nonce SHOULD be chosen uniformly at random by the sender of a compact
block, so that short ID collisions between blocks and senders are independent.
The full transaction ID of a transaction is the 64-byte value consisting of
the transaction’s txid followed by its auth_digest. For a transaction with
version ≥ 5, this is the wtxid used for relay, as defined in ZIP 239
23. For a transaction with version ≤ 4, the auth_digest is the
placeholder value consisting of 32 bytes of 0xFF defined in ZIP 244
28.
Matching against full transaction IDs is exact — they are not subject to
collisions and involve no nonce. A node unable to do short-ID matching (for
example, a pruned node) requests full transaction IDs by setting
full_ids = 1 in its init record, at the cost of larger compact blocks.
Transactions that could not be matched while reconstructing a block from a
compact block are requested with get-tx requests, rather than with dedicated
messages as in BIP 152 (getblocktxn and blocktxn, which identify
transactions by index within the block):
ids_kind = 0, each unmatched short transaction ID
is requested as a SHORTID transaction reference containing the block hash
and the short transaction ID copied from the compact block. Because short
transaction IDs depend on the nonce of the compact block they appeared in,
the responding node interprets them using the nonce of the compact block
it most recently sent to the requesting peer for the identified block. A
node receiving a SHORTID reference for a block for which it has not
recently sent that peer a compact block, or whose short transaction ID
matches no transaction — or more than one transaction — in the identified
block, SHOULD answer that reference with a not-found result.ids_kind = 1 — and likewise for each unmatched
full transaction ID from a get-headers response with tx_ids = 1 (see
get-headers) — each unmatched full transaction ID is
requested as an ordinary transaction reference: a TXID reference
containing the txid if the auth_digest half of the full transaction ID is
the 32-byte 0xFF placeholder (indicating a pre-v5 transaction), or a
WTXID reference containing the full transaction ID otherwise.A node that has sent a peer a compact block, or a get-headers entry carrying
transaction IDs, MUST be able, for a reasonable implementation-defined time
thereafter, to serve that block’s transactions to the same peer via get-tx —
including by SHORTID references in the compact block case, and irrespective
of whether those transactions remain in its mempool.
A node follows the BIP 152 31 protocol flows:
init record had announce = 1):
the sender announces a new block by sending a compact block announcement
directly. To minimize propagation latency, the sender MAY do so as soon as
it has validated the block header (checking proof of work and difficulty),
before fully validating the block. Because a high-bandwidth announcement may
legitimately precede full validation by the announcing peer, a node MUST NOT
assign a misbehavior penalty for a high-bandwidth announcement — a compact
block announcement, or a header announcement substituted for one (see
Block Announcements) — of a block that fails full
validation, provided the block’s header is valid (including its proof of
work); the same exemption covers the block’s transactions served under
Requesting Missing Transactions. A node
SHOULD request high-bandwidth
announcements from at most 3 peers, preferring the peers that most recently
announced blocks to it first; changing the selection requires a new
connection (see Init Record).init record had announce = 0):
blocks are announced to the receiver with header announcements (see
Block Announcements). The receiver MAY then request
the block’s coinbase transaction and transaction IDs — a get-headers
request with tx_ids = 1 whose hash_stop is the announced block hash —
and reconstruct the block, fetching the transactions it is missing with
get-tx; or it MAY request the full block via get-blocks. The receiver
SHOULD use the transaction-ID form only for blocks close to its chain tip
(BIP 152 31 recommends within 5 blocks), and request full blocks
otherwise.Upon receiving a compact block — or the equivalent header, coinbase
transaction, and transaction IDs from a get-headers response with
tx_ids = 1 — a node attempts to reconstruct the block by matching each
transaction ID against the transactions it already holds (in its mempool or
otherwise) — computing short transaction IDs for the held transactions if the
IDs are short (ids_kind = 0), or comparing full transaction IDs directly
otherwise:
get-tx request (see
Requesting Missing Transactions), and
completes reconstruction as the response arrives.get-blocks, and MUST NOT assign a misbehavior penalty solely because
reconstruction failed.An incorrectly reconstructed block fails validation of the block’s merkle root and authorizing data commitment, so short ID matching does not weaken consensus enforcement.
A new block reaches peer B (high-bandwidth mode) and peer C (low-bandwidth mode) from peer A:
A ── block announcements ────▶ B : compact block (header, short
IDs, prefilled coinbase)
B matches the short IDs against transactions it
holds; two are unknown
B ── get-tx ─────────────────▶ A : [SHORTID, SHORTID]
A ── response ───────────────▶ B : the two transactions
B reconstructs the block and validates it
A ── block announcements ────▶ C : header announcement
C ── get-headers ────────────▶ A : locator, hash_stop = block
hash, tx_ids = 1
A ── response ───────────────▶ C : header, coinbase, full
transaction IDs
C matches the IDs, fetches missing transactions
via get-tx, reconstructs, and validates
Nodes synchronize the block chain using a headers-first approach; it is the baseline synchronization method of this protocol, which every node supports. It is the full-validation method: it assumes no trusted data beyond the consensus rules and the genesis block. (An alternative method for nodes with trusted checkpoint data is specified in Checkpointed Synchronization; recommended concrete synchronization strategies are specified in 13.)
get-headers request with a block locator
(typically with tx_ids = 0; see get-headers).get-blocks requests.The following rules apply to headers-first synchronization:
A node MAY synchronize spans of the historical chain against trusted
commitments: checkpoint data — bindings of block heights to block hashes —
obtained through a channel the node already trusts, such as its own binary
or local configuration. The bulk primitives of this protocol —
get-hashes, get-block-range (whose descending delivery authenticates
every block against a committed anchor on arrival), get-tree-roots, and
get-object — exist to serve such procedures; a recommended concrete
strategy is specified in 13. Any checkpoint-based
synchronization procedure MUST obey the following rules.
get-hashes responses fail
verification against the node’s commitments, or reflect a different best
chain; such responses are discarded and MAY be retried with other peers.get-hashes is the same depth, measured the same
way, so that a responder’s hints and a requester’s authentication agree
about which blocks are near enough to the tip to be unsettled.get-hashes.The commitment scheme itself — checkpoint spacing, any chunking, and the hash or signature scheme binding it — is local to the node and out of scope for this protocol.
For headers-first synchronization and near-tip block fetching via
get-blocks, the block download parameters of ZIP 204
32 — the download window, the per-peer in-transit
limit, and the stalling timeout — apply unchanged. On a stall, the node MAY
re-request the block from an alternative peer, cancelling the original
request stream with CANCELLED.
Bulk transfer via get-block-range and get-object is not governed by
those parameters: in-flight volume is bounded in bytes by transport flow
control and by the synchronizing node’s scheduling policy, for which
13 gives recommendations (work unit sizing, per-peer budgets,
and stall detection).
Transaction relay follows an announcement-based protocol:
get-tx request.get-tx response.Announcements use TXID and WTXID references according to transaction
version, as specified in
Transaction References.
To impede network topology inference, transaction announcements SHOULD NOT be sent immediately but SHOULD instead be “trickled” at random intervals. The specific parameters are implementation-defined; for the reference values used by legacy implementations, see ZIP 204 33.
The avoidance of relaying transactions that are about to expire
(TX_EXPIRING_SOON_THRESHOLD) is unchanged from the legacy protocol; see
ZIP 204 33.
Orphan transaction handling and the minimum relay fee are local mempool policy, unchanged from the legacy protocol; see ZIP 204 33.
Address relay allows nodes to discover peers by propagating network address
records through the network, via address announcement streams and get-addr
requests.
Address records SHOULD be subject to rate limiting to prevent address flooding. The specific mechanism is implementation-defined; for the token-bucket reference values used by zcashd, see ZIP 204 34.
A node MAY additionally apply backpressure on the peer’s address announcement stream to bound the rate at which it receives address records.
Relayed addresses are unauthenticated, attacker-suppliable data; an address book that one source can fill leads to eclipse 22. A node SHOULD segment its address book by IP range, in the manner of the Bitcoin Core address manager design:
A node SHOULD also diversify its outbound connections across groups: for reference, Bitcoin Core and zcashd make at most one outbound connection per group. Outbound peer selection SHOULD NOT be biased toward the addresses most recently received, since those are the easiest for an attacker to have planted.
Addresses of overlay networks (TORV3, I2P) have no IP-range structure and
are free to generate, so bucketing cannot bound an attacker’s share of them.
A node SHOULD treat each overlay network as a separate bounded segment of its
address book, and a node supporting both IP-based and overlay transports
SHOULD retain a minimum number of outbound connections on IP-based
transports.
The specific broadcast intervals are implementation-defined; for the reference values used by zcashd, see ZIP 204 34.
How a node determines its own externally reachable addresses is out of scope
for this ZIP; the legacy addr_recv-based self-discovery is removed (see
Init Record).
Peer misbehavior is handled by two mechanisms, split by severity. Unambiguous
violations of “MUST”-level requirements — malformed records, invalid stream
usage, exceeded hard limits — are connection errors: the node closes the
connection immediately with the indicated error code (PROTOCOL_ERROR or
FLOOD unless otherwise specified), with no score kept. Violations for which
immediate disconnection would be disproportionate, or which an
honest-but-buggy peer or a peer on a different chain could commit, instead
accumulate a per-peer misbehavior score that leads to disconnection and
banning only when it crosses a threshold.
Every misbehavior penalty — including any implementation-defined penalty assigned under the final row of the table below — is constrained by the following principle: a node MUST NOT assign a misbehavior penalty unless the violation is provable from the received data together with data the node has independently authenticated, does not depend on the node’s own chain view, mempool contents, clock, or local policy, and could not be committed by a conformant peer acting on honest but divergent state. Behavior that fails this test — however suspicious — is instead handled by discarding the data, refusing or cancelling streams, rate limits and backpressure, or disconnection without a ban. Penalties assigned on weaker evidence are worse than none: they let an attacker who can arrange the appearance of misbehavior induce honest nodes to ban one another, narrowing each victim’s peer set toward the attacker’s own nodes.
The misbehavior score works as follows:
| Points | Violation |
|---|---|
| 20 | get-headers response with more than 160 headers. |
| 20 | Non-contiguous headers in a get-headers response. |
| 20 | get-addr response with more than 1000 address records. |
| 20 | get-hashes metadata (txs or notes) that does not match the downloaded block (see get-hashes). |
| 100 | A get-tree-roots entry that fails verification against authenticated header commitments (see get-tree-roots). |
| 100 | A block delivered by get-block-range whose authorizing data commitment fails verification against its header (see get-block-range). |
| 100 | A get-object object, delivered entirely by the peer, that fails verification against the requested content hash (see get-object). |
| 100 | Announcing a v5 transaction with a TXID reference, or a v4-or-earlier transaction with a WTXID reference, in an announcement stream record or a get-mempool response record (see Transaction References and ZIP 239 23). Wrong-typed references in get-tx requests are exempt. |
| varies | Transaction, block, or header validation failure that is provable under the principle above: an invalid signature or proof, malformed structure, invalid proof of work, or a commitment mismatch, established with full context. The penalty is determined by the severity of the validation error, subject to the exemptions below. |
Validation-failure penalties apply only where the invalidity proves that this peer misbehaved. In particular, a node MUST NOT assign a misbehavior penalty for:
get-blocks, get-block-range, get-tx, get-object):
the responder served exactly the bytes the identifier names, and blame
for the object’s existence lies with the peer that announced it, under
the rules above. (Delivered data that does not match the requested
identifier is covered by each stream type’s own rules.)Score thresholds and banning work as follows:
MISBEHAVIOR error code and ban the
peer’s network address.get-addr responses or address announcements.Bans are keyed by network address, and are only as strong as the cost of acquiring a new address. Banning is a meaningful deterrent for IP-based transports; for overlay networks whose addresses are free to generate (such as Tor onion services), it excludes only the banned address, and inbound connection limits — not ban lists — bound the node’s exposure (see Address Book Management).
Address granularity cuts both ways even on IP transports. A single IPv6 operator typically controls at least a /64, so per-address IPv6 bans are nearly free to evade; a node MAY key IPv6 bans and scores by prefix (a /64 is a reasonable default). Conversely, a single IPv4 address behind carrier-grade NAT may be shared by many independent users, so a ban can exclude bystanders — a reason to keep ban durations bounded rather than to widen their scope.
Scope of transport encryption. Transport encryption protects the confidentiality and integrity of each connection against passive network observers. Peers are deliberately not authenticated (see Certificates), so an active attacker in a position to intercept a connection can terminate the encryption with its own certificate and read or modify that connection’s traffic. The structural defense against an active attacker seeking to control a node’s view of the network is redundancy: connections to multiple, independently discovered peers.
Denial of service. QUIC’s address validation and amplification limits,
including Retry packets, bound the cost that spoofed-source floods can impose
(see QUIC Transport). At the application layer, resource
consumption is bounded by the record payload limit (see Records),
the stream concurrency limits (see
Transport Requirements), the per-request count
limits of each request stream type, and address rate limiting (see
Address Relay); exceeding a hard size or rate limit is a
connection error of type FLOOD.
A count limit is not by itself a bound on work, because the per-item size
varies by orders of magnitude: get-block-range and get-object are
therefore bounded in bytes as well (max_bytes and length), and for
get-blocks, get-tx, and get-headers the responder applies its own byte
budget and may finish early, with the requester re-requesting the remainder.
The general shape of the defense is that a responder is never obliged to
spend unbounded bandwidth, disk reads, or memory on one cheap request, and
that every peer-supplied count or length is checked against its bound before
it is used to size an allocation (see Data Types and Encoding).
Fingerprinting and linkability. A persistent TLS key would allow a node
to be recognized across connections and network locations; a node concerned
about this uses per-connection ephemeral keys (see
Certificates). Connection migration links a node’s traffic
across network paths (see QUIC Transport). The user_agent
field of the init record is a fingerprinting vector; a node MAY advertise a
generic user agent string. The get-addr policy (see get-addr)
impedes fingerprinting of a node’s address book, and trickling (see
Trickling) impedes inference of the network topology and of the
origin of a transaction from announcement timing.
Traffic analysis. QUIC encrypts payloads but conceals neither packet timing and volume nor the IP addresses of the communicating peers. In particular, an adversary observing a node’s traffic may still be able to identify it as the originator of a transaction. The Tor transport (see Tor Transport) defends against this adversary when it is local — an observer of the node’s network, or the remote peer itself, learns neither the node’s IP address nor which peers it communicates with — but not against a global adversary correlating traffic timing across the Tor network. The anticipated Nym mixnet transport (see Deployment) is directed at that stronger adversary.
Length leakage. Encryption conceals the content of application data but
not its length: neither TLS over TCP nor QUIC pads application data by
default, so encrypted packet sizes leak plaintext lengths. (QUIC defines
PADDING frames 6, but an implementation must use them explicitly.)
The Tor transport is coarser — Tor pads relay cells to a 509-byte body —
so leakage there is granular rather than byte-exact. To remove the
sharpest instance of this channel, every request format in this protocol
whose fields are position-fixed (get-hashes, get-block-range,
get-tree-roots, get-object, and the empty get-addr and get-mempool
requests) uses fixed-length integer encodings: every request of a given
type has a single wire length, smaller than a Tor cell body, so its
encrypted size reveals at most the stream type and never the field values,
and there is no way to misimplement the encoding so that it leaks lengths
but still interoperates. Requests that carry variable-length lists
(get-headers, get-blocks, get-tx) and responses necessarily reveal
their gross size; a node MAY additionally use QUIC PADDING frames to
obscure those lengths, at a bandwidth cost this specification does not
mandate.
Tor. On a Tor transport connection the initiator is anonymous — it
reveals no network address to the responder or to network observers — so a
node SHOULD prefer the Tor transport (where available) for the traffic most
sensitive to linkage, above all announcing transactions it originated. The
responder’s onion address is a stable, linkable identifier, and onion
addresses are free to generate, which weakens address-based banning and
address book protections for TORV3 addresses (see
Misbehavior and Banning and
Address Book Management).
Mixnets. The two kinds of traffic most sensitive to linkage are mempool
transaction propagation — announcing a transaction links the announcing
node to it, and a get-mempool subscription reveals interest in the
mempool’s contents — and address book queries, whose get-addr
request/response pattern is a fingerprinting and mapping primitive. A node
implementer SHOULD carry both over a mixnet transport once one is
available (the anticipated Nym transport; see Deployment),
and in the interim SHOULD carry them over the Tor transport where
available, per the preference above. Mixnet delay and cover traffic
address the timing correlation that onion routing alone does not.
Synchronization. Headers-first synchronization trusts proof of work: forging a history requires outspending the honest chain’s accumulated work over the forged span, which an eclipsing attacker with sufficient hash power can in principle do. Checkpointed synchronization trusts the node’s commitment, which shares a trust base with its binary: below its checkpoints, an attacker who cannot break the block hash function cannot cause acceptance of a false history. Under either method, an attacker controlling a node’s connections can withhold data and stall synchronization; the mitigations are redundancy and re-requesting from alternative peers (see Block Download Parameters).
Eclipse attacks. An attacker that controls all of a node’s connections controls its view of the block chain and mempool. The structural defenses are segmentation of the address book by IP range and diversity in outbound peer selection (see Address Book Management and 22), persisted addresses (see Peer Discovery), and rate-limited, validated address relay (see Address Relay). Overlay addresses such as onion services deserve particular caution here, since an attacker can generate them without limit (see Address Book Management).
This ZIP replaces the legacy protocol without a compatibility bridge: nodes implementing this protocol do not interoperate with nodes implementing the legacy protocol. Deployment is coordinated through the network upgrade mechanism 3: the protocol version from which this protocol is in effect will be assigned to a network upgrade, and the epoch enforcement of ZIP 201 27 retires legacy-protocol peers at activation, as with any other network upgrade.
Because there is no compatibility bridge, an implementation deploying this protocol is expected to also implement the legacy protocol during the transition: before activation, a node participates in the network over the legacy protocol while also listening on the QUIC transport, so that DNS seeders and upgraded peers can discover and exercise QUIC endpoints ahead of activation. The QUIC transport uses UDP where the legacy transport uses TCP, so both can be served concurrently on the same port number and address. At activation, epoch enforcement retires legacy-protocol connections, and the node continues on this protocol alone.
DNS seeders are expected to probe and serve the QUIC endpoints of nodes implementing this ZIP.
The protocol semantics are specified against the abstract stream layer of Stream Layer; QUIC and Tor are the transports defined by this ZIP. Future revisions are expected to define additional transports to be used simultaneously with them; in particular, a transport carrying the stream layer over the Nym mixnet 36 is anticipated, providing network-level metadata protection (resistance to traffic analysis of which peers are communicating) stronger than onion routing provides.
Note: The transports are complementary rather than alternatives: a node is expected to use QUIC for latency-critical bulk relay and synchronization, and Tor (eventually Nym) where anonymity matters more than latency, selected per connection according to the traffic it will carry.
A TLA+ formal specification of the legacy protocol’s connection lifecycle is available at 37; it has not yet been updated for the protocol specified here.
Information on BCP 14 — “RFC 2119: Key words for use in RFCs to Indicate Requirement Levels” and “RFC 8174: Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words” ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3]. Section 3.12: Mainnet and Testnet ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3]. Section 3.3: The Block Chain ↩︎
RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport ↩︎
RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3]. Section 7.1: Transaction Encoding and Consensus ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3]. Section 7.6: Block Header Encoding and Consensus ↩︎
RFC 7301: Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension ↩︎
RFC 7250: Using Raw Public Keys in Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS) ↩︎
ZIP 204: Zcash P2P Network Protocol — Data Types and Encoding ↩︎
Ethan Heilman, Alison Kendler, Aviv Zohar, Sharon Goldberg. Eclipse Attacks on Bitcoin’s Peer-to-Peer Network. 24th USENIX Security Symposium, 2015. ↩︎
bit \(k\) is the bit with numeric weight \(2^k\kern-0.05em\textsf{.}\) ↩︎
ZIP 204: Zcash P2P Network Protocol — Assigning Protocol Versions to Network Upgrades ↩︎
ZIP 204: Zcash P2P Network Protocol — Network Upgrade Epoch Enforcement ↩︎
ZIP 204: Zcash P2P Network Protocol — Block Download Parameters ↩︎
ZIP 204: Zcash P2P Network Protocol — Misbehavior and Banning ↩︎