# Draft: Version 2 Zcash P2P Network Protocol

Status: Draft

Owners: Arya, Janito

Snapshot: a34cfc091434275120ff017c92c3b50d3cef623a

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

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

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

## Document parts

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

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

Preamble

**ZIP**: XXX

**Title**: Version 2 Zcash P2P Network Protocol

**Owners**: 

- [Arya](mailto:arya@zfnd.org)
- [Janito](mailto:janito@zfnd.org)

**Status**: Draft

**Category**: Network

**Created**: 2026-07-31

**License**: MIT

**Discussions-To**: [https://github.com/zcash/zips/issues/352](https://github.com/zcash/zips/issues/352)

<a id="terminology"></a>

## Terminology

The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", "MAY", and
"RECOMMENDED" in this document are to be interpreted as described in BCP 14
<a id="user-content-fnref-bcp14"></a>
[1](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-bcp14) 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. <a id="user-content-fnref-protocol-networks"></a>
[2](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-networks)

The term "network upgrade" is to be interpreted as described in ZIP 200.
<a id="user-content-fnref-zip-0200"></a>
[3](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0200)

The term "block chain" in this document is to be interpreted as described in
section 3.3 of the Zcash Protocol Specification. <a id="user-content-fnref-protocol-blockchain"></a>
[4](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockchain)

The term "legacy protocol" in this document refers to the Zcash P2P network
protocol specified in ZIP 204 <a id="user-content-fnref-zip-0204"></a>
[5](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204).

peer
:   A network node that participates in the Zcash P2P protocol by maintaining
connections to other peers over one or more supported transports and
exchanging protocol data over streams.

transport
:   A concrete mechanism, such as QUIC <a id="user-content-fnref-rfc9000"></a>
[6](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9000) or Tor onion services
<a id="user-content-fnref-tor-rend-spec"></a>
[7](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-tor-rend-spec), that carries connections between peers and provides the
stream layer of this protocol (see [Stream Layer](#streamlayer) and
[Transports](#transports)).

connection
:   A session between two peers over a supported transport, over which P2P
streams are exchanged.

initiator
:   The peer that initiated a connection (the transport-level client).

responder
:   The peer that accepted a connection (the transport-level server). In the
context of a single request stream, *responder* instead denotes the peer
serving the request, irrespective of which peer accepted the connection
(see [Request Streams](#requeststreams)); which sense is meant is
indicated by context.

requester
:   The peer that opened a request stream and sends the request on it. Either
the initiator or the responder of a connection may be the requester on a
given request stream.

stream
:   An ordered, reliable byte stream within a connection, provided by the
transport. Streams are typed by their first byte (see
[Stream Layer](#streamlayer)).

request stream
:   A bidirectional stream carrying a single request and its response.

announcement stream
:   A long-lived unidirectional stream carrying unsolicited announcements
(blocks, transactions, or peer addresses).

record
:   A length-delimited unit of data on an announcement or handshake stream.

protocol version
:   A 32-bit integer identifying the set of protocol features supported by a
node. Higher values indicate support for more recent features.

<a id="abstract"></a>

## Abstract

This ZIP specifies version 2 of the Zcash peer-to-peer network protocol, a
successor to the protocol specified in ZIP 204 <a id="user-content-fnref-zip-0204-2"></a>
[5](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204). 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.

<a id="motivation"></a>

## Motivation

ZIP 204 <a id="user-content-fnref-zip-0204-3"></a>
[5](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204) 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
<a id="user-content-fnref-rfc9000-2"></a>
[6](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9000) and one carried over Tor onion services <a id="user-content-fnref-tor-rend-spec-2"></a>
[7](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-tor-rend-spec). 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 <a id="user-content-fnref-rfc8446"></a>
  [8](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc8446) <a id="user-content-fnref-rfc9001"></a>
  [9](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9001), and the Tor transport inherits the
  encryption of onion service connections. (Bitcoin addressed the same gap with
  a bespoke encrypted transport, BIP 324 <a id="user-content-fnref-bip-0324"></a>
  [10](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-bip-0324); 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](/draft/draft-arya-jvff-p2p-quic-transport?part=3#deployment).

This ZIP does not specify the encoding of transactions and block headers; for
those, refer to the Zcash Protocol Specification <a id="user-content-fnref-protocol-txnencoding"></a>
[11](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-txnencoding)
<a id="user-content-fnref-protocol-blockheader"></a>
[12](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockheader). 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 <a id="user-content-fnref-protocol-blockheader-2"></a>
[12](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockheader), and is specified in
[Serialized Blocks](#serializedblocks).

<a id="changes-from-the-legacy-protocol"></a>

## Changes from the Legacy Protocol

This section is non-normative. Relative to the legacy protocol specified in
ZIP 204 <a id="user-content-fnref-zip-0204-4"></a>
[5](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204), this ZIP:

- Replaces network magic bytes with transport-level network identification (for
  the QUIC transport, ALPN protocol identifiers; for the Tor transport, an
  in-band network identifier; see [QUIC Transport](#quictransport) and
  [Tor Transport](#tortransport)).

- Removes per-message headers entirely: the 12-byte command string is replaced
  by a 1-byte stream type, and the length and `SHA-256d` checksum fields are
  subsumed by QUIC framing and authenticated encryption.

- Replaces the `version`/`verack` handshake with a single `init` record
  exchange on a dedicated handshake stream (see
  [Connection Handshake](#connectionhandshake)). The `timestamp`, `addr_recv`,
  and `addr_from` fields are removed.

- Removes `ping` and `pong`; QUIC provides keep-alives and RTT measurement.

- Removes `reject`; QUIC application error codes are used instead (see
  [Application Error Codes](#applicationerrorcodes)).

- Removes `inv`, `getdata`, and `notfound`; announcements are carried on
  dedicated unidirectional streams, and object requests carry per-item results.

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

- Removes the deprecated `alert` message.

- Removes BIP 37 Bloom filtering (`filterload`, `filteradd`, `filterclear`)
  and the `NODE_BLOOM` service flag.

- Removes the legacy `addr` message and `CAddress`/`CService` encodings; the
  `addrv2` address encoding of ZIP 155 <a id="user-content-fnref-zip-0155"></a>
  [14](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0155) is the only address format.

- Retains divided block relay — header-based block announcement and compact
  block relay — recast onto announcement streams and request streams, with a
  block's coinbase transaction and transaction IDs additionally available in
  `get-headers` responses (see [Block Relay](#blockrelay)).

<a id="specification"></a>

## Specification

This specification is organized bottom-up. [Transports](#transports) defines
how connections are established and secured (QUIC and Tor);
[Stream Layer](#streamlayer) defines the typed streams that carry all
application data, and [Data Types and Encoding](#datatypesandencoding) the
shared encodings. [Connection Handshake](#connectionhandshake) and
[Protocol Versioning](#protocolversioning) cover connection setup.
[Request Stream Types](#requeststreamtypes) and
[Announcement Stream Types](#announcementstreamtypes) specify the individual
request and announcement formats, and [Block Relay](#blockrelay),
[Synchronization](/draft/draft-arya-jvff-p2p-quic-transport?part=3#synchronization), [Transaction Relay](#transactionrelay),
and [Address Relay](#addressrelay) the behaviors built on them.

<a id="network-parameters"></a>

### Network Parameters

#### Networks

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](#transports)); this replaces the
network magic bytes of the legacy protocol.

#### DNS Seeds

The DNS seed hostnames used for initial peer discovery on each network are
those listed in ZIP 204 <a id="user-content-fnref-zip-0204-dnsseeds"></a>
[15](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-dnsseeds). Regtest does not use DNS seeds
or hardcoded seed nodes.

<a id="peer-discovery"></a>

### Peer Discovery

A node discovers peers through the following mechanisms:

1. **Persisted addresses.** A node SHOULD persist the peer addresses it learns
   (see [Address Relay](#addressrelay)) across restarts, and on startup SHOULD
   attempt connections to previously learned addresses before consulting
   seeds.

2. **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.

3. **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.

4. **Address relay.** Connected peers exchange address information using
   `get-addr` request streams and address announcement streams (see
   [`get-addr`](#get-addr) and
   [Address Announcements](#addressannouncements)). 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](#tortransport)) — are learned through address relay or provided
by local configuration.

<a id="transports"></a>

### Transports

The protocol semantics in this document are defined against the abstract stream
layer of [Stream Layer](#streamlayer). 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](#quictransport)), and one carried over Tor onion services (see
[Tor Transport](#tortransport)). Future revisions may define additional
transports — a transport carrying the stream layer over the Nym mixnet is
anticipated (see [Deployment](/draft/draft-arya-jvff-p2p-quic-transport?part=3#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:

- a secure channel between the two peers: confidentiality and integrity for
  all application data;

- identification of the network (see [Networks](#networks)) during connection
  establishment, such that a connection between peers on different networks
  fails before any application data is exchanged;

- a form of address by which peers reachable over the transport are identified,
  encodable as a network address record (see
  [Network Address Record](#networkaddressrecord));

- the stream operations and guarantees of
  [Transport Requirements](#transportrequirements).

#### Connection Management

The transport provides keep-alives and round-trip time measurement; the legacy
`ping`/`pong` messages have no equivalent in this protocol.

- A node SHOULD use an idle timeout of at least 120 seconds, and SHOULD use the
  transport's keep-alive mechanism to keep connections it wishes to retain from
  idling out. The specific values are implementation-defined.

- A node MUST close a connection on which the application handshake (see
  [Connection Handshake](#connectionhandshake)) has not completed within a
  bounded time, measured from the establishment of the transport connection so
  that it covers everything a peer sends beforehand — including the Tor
  transport's connection preamble (see [Tor Transport](#tortransport)). The
  specific timeout is implementation-defined; values between 3 and 60 seconds
  were used by legacy implementations. Without such a timeout, a peer that
  connects and then sends nothing holds a connection slot indefinitely, and the
  transport's keep-alive mechanism — answered by the transport stack rather
  than by the application — will report the connection healthy.

- A node SHOULD bound the number of concurrent inbound connections it accepts,
  and SHOULD bound the share of them originating from any one transport, so
  that inbound slots on a transport whose addresses are free to generate cannot
  displace the rest.

- Per-request timeouts are implementation-defined and are applied per stream
  (see [Request Streams](#requeststreams)).

The specific connection limits are implementation-defined; the legacy
reference values in ZIP 204 <a id="user-content-fnref-zip-0204-5"></a>
[5](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204) 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](#initrecord).

#### QUIC Transport

The QUIC transport uses QUIC version 1 <a id="user-content-fnref-rfc9000-3"></a>
[6](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9000) over UDP, secured with
TLS 1.3 <a id="user-content-fnref-rfc8446-2"></a>
[8](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc8446) as specified by RFC 9001 <a id="user-content-fnref-rfc9001-2"></a>
[9](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9001).

- A node MUST NOT use 0-RTT (early data). Note: a peer that attempts to use
  0-RTT MUST have its connection closed — a responder MUST NOT enable the
  acceptance of early data, and a node whose peer nevertheless attempts to
  use 0-RTT MUST close the connection with the `PROTOCOL_ERROR` error code.
  (0-RTT data is replayable by an attacker.)

- QUIC datagrams <a id="user-content-fnref-rfc9221"></a>
  [16](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9221) are not used by this protocol. A node MUST ignore
  the peer's `max_datagram_frame_size` transport parameter and MUST NOT send
  DATAGRAM frames.

- A node MAY support connection migration as specified in RFC 9000. Migration
  links the node's traffic across the old and new network paths; a node for
  which that linkage is a privacy concern SHOULD establish a fresh connection
  instead of migrating.

- The address validation and amplification limits of RFC 9000 (including Retry
  packets) apply; a responder under load SHOULD use Retry to validate client
  addresses before committing state.

- QUIC's loss recovery provides round-trip time measurement, and QUIC PING
  frames provide keep-alives. A node using the QUIC transport SHOULD advertise
  a `max_idle_timeout` transport parameter consistent with the idle timeout of
  [Connection Management](#connectionmanagement).

##### Network Identification

The network of a QUIC connection is identified by an Application-Layer Protocol
Negotiation (ALPN) <a id="user-content-fnref-rfc7301"></a>
[17](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc7301) 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.

##### Default Ports

The QUIC transport uses the default port numbers of the legacy protocol,
as specified in ZIP 204 <a id="user-content-fnref-zip-0204-ports"></a>
[18](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-ports), as UDP port numbers.

##### Certificates

Connections are encrypted but endpoints are not authenticated: the goal is
protection against passive network observers, not endpoint identity.

- A node presents a self-signed X.509 certificate, or a raw public key
  <a id="user-content-fnref-rfc7250"></a>
  [19](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc7250), for an Ed25519 or ECDSA P-256 key. The key MAY be ephemeral or
  persistent, at the node's option; a node concerned about cross-connection
  linkability SHOULD use per-connection ephemeral keys.

- A node MUST be prepared to accept both certificate encodings (X.509 and raw
  public key) and both key types from its peer.

- A node MUST NOT require the peer's certificate to chain to any certification
  authority, and MUST NOT reject a connection on the basis of certificate
  contents (subject names, expiry, or extensions).

- Client certificates are not requested: the responder MUST NOT require TLS
  client authentication.

##### Bulk Transfer Performance

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

- A node SHOULD size its flow control windows to the bandwidth-delay
  product it intends to sustain: at least 16 MiB of per-stream receive
  window and 64 MiB of connection receive window are RECOMMENDED. Default
  windows tuned for interactive traffic cap bulk throughput at a small
  fraction of link capacity once round-trip time is non-negligible.

- A node serving bulk streams SHOULD use a paced, model-based congestion
  controller suited to high bandwidth-delay paths; loss-triggered
  controllers collapse under modest jitter at these transfer sizes.

- Implementations SHOULD use platform UDP batching and segmentation
  offloads where available; per-packet processing, not cryptography, is the
  dominant CPU cost of QUIC bulk transfer.

- One QUIC connection is typically processed by one core, bounding
  per-connection throughput. Synchronizing nodes obtain aggregate
  throughput by downloading from several peers concurrently rather than by
  tuning a single connection (see <a id="user-content-fnref-draft-sync-3"></a>
  [13](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-draft-sync)).

##### Stream Layer Mapping

The abstract stream layer of [Transport Requirements](#transportrequirements)
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. |

#### Tor Transport

The Tor transport carries connections over the Tor network <a id="user-content-fnref-tor"></a>
[20](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-tor) as
connections to version 3 onion services <a id="user-content-fnref-tor-rend-spec-3"></a>
[7](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-tor-rend-spec). It serves traffic for
which the metadata protection of onion routing outweighs its latency cost (see
[Deployment](/draft/draft-arya-jvff-p2p-quic-transport?part=3#deployment) and
[Security and Privacy Considerations](#securityandprivacyconsiderations)), and
allows a node to participate in the network without exposing an IP address.

- A node that accepts inbound connections over this transport runs a version 3
  onion service and advertises its address as a `TORV3` network address record
  (see [Network Address Record](#networkaddressrecord)); the record's `port`
  field is the onion service virtual port, which SHOULD be the default port of
  the network (see [Default Ports](#defaultports)).

- The initiator connects to the responder's onion service through the Tor
  network. The initiator needs no onion service of its own and is anonymous to
  the responder.

- The secure channel is provided by Tor itself: onion service connections are
  end-to-end encrypted, and the connection is authenticated to the responder's
  onion address (the Ed25519 key that the `TORV3` address encodes). TLS is not
  used inside the connection, and the initiator is deliberately not
  authenticated (cf. [Certificates](#certificates)).

- The transport provides a single ordered bytestream per connection; the
  stream layer is realized on top of it by the framing layer of
  [Stream Framing](#streamframing).

*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](/draft/draft-arya-jvff-p2p-quic-transport?part=3#deployment) for transport selection).

##### Connection Preamble

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](#networkidentification)): `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](#streamframing)). 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](#transportrequirements).

The preamble precedes every frame, and therefore precedes the record payload
limit of [Records](#records) and the malformed-frame rule of
[Stream Framing](#streamframing); 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:

- The `network` string MUST NOT exceed 16 bytes.

- Each of the four flow control fields MUST NOT exceed 2^62 − 1, the
  corresponding limit in QUIC <a id="user-content-fnref-rfc9000-4"></a>
  [6](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-rfc9000).

- A node MUST close the connection immediately, without sending frames, if a
  preamble field exceeds its bound, if the preamble is not complete within the
  handshake timeout below, or if a CompactSize in the preamble is not
  canonically encoded (see [Data Types and Encoding](#datatypesandencoding)).

The handshake timeout of
[Connection Management](#connectionmanagement) 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](#misbehaviorandbanning)), 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.

##### Stream Framing

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

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

<a id="stream-layer"></a>

### Stream Layer

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.

#### Transport Requirements

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:

- Either peer can **open** streams of either kind at any time. The receiver of
  a stream can tell which peer opened it and of which kind it is.

- Data written to a stream is delivered reliably and in order within that
  stream. Streams are mutually independent: delay or loss on one stream MUST
  NOT prevent delivery of data on other streams.

- A sender can **finish** its sending direction of a stream, signalling that no
  more data will be sent on it.

- A peer can **reset** a stream it is sending on, abandoning it with an
  application error code (see
  [Application Error Codes](#applicationerrorcodes)), and can **cancel** the
  peer's sending direction of a stream, requesting with an application error
  code that the peer stop sending.

- Either peer can **close** the connection with an application error code.

- The receiver of a stream can apply per-stream backpressure to bound how much
  data it must buffer.

- Each peer can bound the number of streams of each kind that the other peer
  may have concurrently open. A node SHOULD allow at least 32 concurrent
  bidirectional streams, and SHOULD allow at least 8 concurrent unidirectional
  streams (sufficient for the announcement streams plus headroom for future
  types).

How each transport realizes these operations is specified in the corresponding
transport section (for QUIC, see [Stream Layer Mapping](#streamlayermapping)).

#### Stream Types

| Code | Name | Kind | Description |
| --- | --- | --- | --- |
| `0x00` | Handshake | Bidirectional, initiator only | Connection handshake and control (see [Connection Handshake](#connectionhandshake)). |
| `0x01` | [`get-headers`](#get-headers) | Bidirectional | Request block headers, optionally with transaction IDs. |
| `0x02` | [`get-blocks`](#get-blocks) | Bidirectional | Request full blocks. |
| `0x03` | [`get-tx`](#get-tx) | Bidirectional | Request transactions. |
| `0x04` | [`get-addr`](#get-addr) | Bidirectional | Request peer addresses. |
| `0x05` | [`get-mempool`](#get-mempool) | Bidirectional | Subscribe to mempool contents. |
| `0x06` | [`get-hashes`](#get-hashes) | Bidirectional | Request best-chain block hashes and per-block sync-cost metadata. |
| `0x07` | [`get-block-range`](#get-block-range) | Bidirectional | Stream a contiguous range of blocks, verified against an anchor hash. |
| `0x08` | [`get-tree-roots`](#get-tree-roots) | Bidirectional | Request per-block note commitment tree roots for a height range. |
| `0x09` | [`get-object`](#get-object) | Bidirectional | Request a range of a content-addressed synchronization artifact. |
| `0x10` | Block announcements | Unidirectional | Announce new blocks (see [Block Announcements](#blockannouncements)). |
| `0x11` | Transaction announcements | Unidirectional | Announce new transactions (see [Transaction Announcements](#transactionannouncements)). |
| `0x12` | Address announcements | Unidirectional | Gossip peer addresses (see [Address Announcements](#addressannouncements)). |

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

#### Request Streams

A request stream is a bidirectional stream carrying exactly one request and its
response:

1. The requester opens a bidirectional stream, writes the stream type byte
   followed by the request in the format defined for that type, and finishes
   its sending direction.

2. The responder writes the response in the format defined for that type and
   finishes its sending direction. (For a stream type that defines an
   open-ended response, the responder instead keeps its sending direction open;
   see [`get-mempool`](#get-mempool).)

Either peer may open request streams; there is no client/server asymmetry
beyond the rules stated for individual stream types.

- A responder MAY begin serving a request as soon as the request is
  syntactically complete, without waiting for the requester to finish its
  sending direction.

- A responder that cannot or will not serve a request MAY reset its sending
  direction of the stream with the `REFUSED` error code instead of sending a
  response.

- A requester that no longer needs a response SHOULD cancel the responder's
  sending direction with the `CANCELLED` error code; the responder SHOULD then
  reset the stream and abandon work on the request.

- A reset or refused request stream is never resumed or reused. A requester
  that retries a request after a reset does so by opening a new stream, and
  the next byte the requesting side sends — the first byte of that new stream
  — MUST be the stream type byte, as for any other stream.

- A node SHOULD apply an implementation-defined timeout to each outstanding
  request, cancelling the stream if the response does not complete in time.

- A node MUST NOT send more than one request on a stream. Unless the stream
  type defines an open-ended response, any data following a complete request
  or response on a stream is a connection error of type `PROTOCOL_ERROR`.

- A request or response that does not conform to the format defined for its
  stream type — including an unrecognized `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](#transportrequirements)).

#### Announcement Streams

An announcement stream is a long-lived unidirectional stream carrying a
sequence of records for one announcement topic.

- After the connection handshake completes, each peer SHOULD open one
  announcement stream of each type it intends to send announcements on, and
  keep it open for the life of the connection.

- A node MUST NOT open more than one announcement stream of a given type in
  the same direction at a time. A second concurrent stream of the same type is
  a connection error of type `PROTOCOL_ERROR`. If an announcement stream is
  reset or finished, the sender MAY open a replacement.

- Announcements are best-effort: if transport backpressure prevents a record
  from being written promptly, the sender MAY drop the announcement rather
  than queueing it indefinitely.

- A record on an announcement stream that does not conform to the format
  defined for its stream type — a *nonconforming record* — is a connection
  error of type `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.)

#### Records

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

#### Application Error Codes

The following application error codes are used when closing a connection,
resetting a stream, or cancelling a peer's sending direction (see
[Transport Requirements](#transportrequirements)):

| 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](#networkupgradeepochenforcement)). |
| `0x04` | `SELF_CONNECTION` | The connection is to the node itself (see [Init Record](#initrecord)). |
| `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](#misbehaviorandbanning)). |
| `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`.

<a id="data-types-and-encoding"></a>

### Data Types and Encoding

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 <a id="user-content-fnref-zip-0204-datatypes"></a>
[21](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0204-datatypes). 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 2^64 − 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.

#### Serialized Blocks

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 <a id="user-content-fnref-protocol-blockheader-3"></a>
[12](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockheader). 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 <a id="user-content-fnref-protocol-blockheader-4"></a> [12](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-blockheader) (including the Equihash solution). |
| varies | `tx_count` | Number of transactions in the block (CompactSize). |
| varies | `txns` | `tx_count` serialized transactions <a id="user-content-fnref-protocol-txnencoding-2"></a> [11](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-protocol-txnencoding), in block order. |

#### Network Address Record

Peer addresses use the `addrv2` address record specified in ZIP 155
<a id="user-content-fnref-zip-0155-2"></a>
[14](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0155); 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](#tortransport)). 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 <a id="user-content-fnref-eclipse"></a>
[22](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-eclipse) that operates from inside the
buckets, where the bucketing of
[Address Book Management](#addressbookmanagement) affords no protection. A
node therefore:

- MUST clamp the `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.

- SHOULD NOT relay, and SHOULD eventually evict, a record whose clamped
  `time` is more than 30 days in the past.

- SHOULD NOT make the relayed `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.

#### Transaction References

Transactions are identified in announcements and requests by *transaction
references*. Per ZIP 239 <a id="user-content-fnref-zip-0239"></a>
[23](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0239), 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 <a id="user-content-fnref-zip-0239-2"></a> [23](/draft/draft-arya-jvff-p2p-quic-transport?part=3#user-content-fn-zip-0239). |
| `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](#shorttransactionids)). 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](#misbehaviorandbanning)). 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](#requestingmissingtransactions)); 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.

<a id="service-flags"></a>

### Service Flags

Service flags are advertised in the `services` field of `init` records and
network address records. Bit numbering is as in ZIP 204

## Citation relationships

Cites: [ZIP 155](https://zip-tools-web.harpocrates.workers.dev/zip/155), [ZIP 200](https://zip-tools-web.harpocrates.workers.dev/zip/200), [ZIP 201](https://zip-tools-web.harpocrates.workers.dev/zip/201), [ZIP 204](https://zip-tools-web.harpocrates.workers.dev/zip/204), [ZIP 221](https://zip-tools-web.harpocrates.workers.dev/zip/221), [ZIP 239](https://zip-tools-web.harpocrates.workers.dev/zip/239), [ZIP 244](https://zip-tools-web.harpocrates.workers.dev/zip/244)

Cited by: None