12. P2P protocol
12.1 Transport
TCP, then a hybrid key exchange:
initiator → responder: magic(4) | 0x01 | x25519_pub(32) | ML-KEM-768 encapsulation key(1184)
responder → initiator: magic(4) | 0x01 | x25519_pub(32) | ML-KEM-768 ciphertext(1088)
okm = HKDF-SHA3-256(salt=magic, ikm = x25519_ss ‖ mlkem_ss,
info = "ARRK p2p v1" ‖ SHA3-256(hello ‖ reply), L = 96)
k_i→r = okm[0:32] k_r→i = okm[32:64] session_id = okm[64:96]
frame = u32 ciphertext_len ‖ ChaCha20-Poly1305(k, nonce = 96-bit LE counter, plaintext)
- Counters start at 0 per direction and never repeat. Any authentication failure closes the connection.
- The maximum frame is 33 MiB.
- Peers are not authenticated; consensus validation protects the node.
12.2 Messages
payload = u8 type ‖ body. Protocol version 3 (VERSION.protocol): headers travel as full
headers with their AuxPoW. Peers announcing a lower version are disconnected.
| Type | Name | Body |
|---|---|---|
| 1 | VERSION | protocol u32, services u64, timestamp u64, nonce u64, user_agent varstr(≤256), best_height u32, listen_port u16, genesis 32 |
| 2 | VERACK | — |
| 3 / 4 | PING / PONG | nonce u64 |
| 5 | GETBLOCKS | varint n (≤101) × hash32, stop hash32 (served for compatibility; v2 nodes sync with GETHEADERS) |
| 6 / 7 / 10 | INV / GETDATA / NOTFOUND | varint n (≤50,000) × [type u8 (1 tx, 2 block), hash32] |
| 8 / 9 | BLOCK / TX | serialized object |
| 11 / 12 | GETADDR / ADDR | — / varint n (≤1000) × [host varstr(≤255), port u16] |
| 13 | GETHEADERS | varint n (≤101) × hash32 (locator), stop hash32 |
| 14 | HEADERS | varint n (≤2000) × full header (§9); replies stop at a 16 MiB byte budget |
| 15 | SENDCMPCT | mode u8 (0 = announce blocks with INV, 1 = push CMPCTBLOCK) |
| 16 | CMPCTBLOCK | full header ‖ nonce u64 ‖ varint s (≤100,000) × shortid (6) ‖ varint p (≤100,000) × [index u32 ‖ varbytes tx] |
| 17 | GETBLOCKTXN | block_hash 32 ‖ varint n (≤100,000) × index u32 |
| 18 | BLOCKTXN | block_hash 32 ‖ varint n (≤100,000) × varbytes tx |
| 23 | CHECKPOINT | a (partial) checkpoint (§9.8); relayed when it adds signatures; final checkpoints and every hand-over are sent after VERACK |
| 24 | PACKAGE | varint n (2–25) × varbytes tx: parent and child for fee bumping (§13.2) |
Service bits (VERSION services):
| Bit | Name | Meaning |
|---|---|---|
| 0 | NODE_NETWORK | serves every block |
| 2 | NODE_FILTERS | serves compact block filters (§12.5) |
| 10 | NODE_NETWORK_LIMITED | a pruned node: serves at least the last 288 blocks below its best height (as BIP159) |
A node MUST NOT ask a peer without NODE_NETWORK for a block 288 or more below that peer's
best height. A peer asked for a block it does not keep answers NOTFOUND.
Handshake rules:
- A different
genesisor a self-connect nonce closes the connection. - After VERACK each side sends SENDCMPCT(1). If the peer's
best_heightis above our best known header, we start headers-first sync.
Misbehaviour:
- Undecodable messages, consensus-invalid blocks or transactions, and invalid headers score 100 → 24 h ban.
- Policy rejections (fees, mempool conflicts) and
time-too-newMUST NOT be penalised.
12.3 Headers-first sync
-
A block locator lists up to 10 hashes from our best header back through the header tree, then the active chain's locator (dense for 10 blocks, then exponentially sparse, ending at genesis).
-
GETHEADERS: the responder finds the first locator hash on its active chain and answers with HEADERS for up to 2,000 following blocks (stopping at
stopif non-zero). -
Accepting headers. Headers in one message MUST be continuous (
prev_hash= previous header's hash), otherwise the peer scores 20. Each new header MUST:- extend a known header or block (an unconnecting first header triggers a GETHEADERS with our locator; 10 unconnecting messages score 20);
- have version 1, a target ≤ the pow limit, and valid proof of work;
- pass the contextual header rules of §9.2: height, expected bits, and timestamp > median time past.
A failure scores 100. A header more than
max_future_driftahead of local time stops processing without penalty. -
Header tree. Accepted headers whose blocks we don't have go into a header tree that records cumulative work. The best header is the tree entry or active tip with the most work.
-
More headers: a full HEADERS message (2,000) triggers the next GETHEADERS, from its last header.
-
Downloading bodies. While the best header has more work than the active tip, the node downloads the blocks on its path:
- at most the first 128 missing blocks past the fork point;
- at most 16 requests in flight per peer, spread over peers whose best height covers the block;
- requests unanswered within 30 s are reassigned without penalty.
Blocks may arrive in any order. They wait in the orphan pool until their parent connects. A block that fails validation is removed from the header tree together with its descendants.
12.4 Compact block relay
-
When a block becomes the new active tip, it is pushed as CMPCTBLOCK to peers in mode 1, and announced with INV to the others. Several blocks connected at once are announced with INV.
-
Short IDs:
shortid(tx) = TH("cmpct/shortid", block_hash ‖ u64 nonce ‖ wtxid)[0:6], with a fresh random nonce per message. Using the wtxid means a transaction with different witness data cannot be substituted. -
Prefilled transactions: the coinbase (index 0) is always prefilled. Prefilled indices are strictly increasing, and
s + pis the block's transaction count; short IDs fill the remaining positions in order. -
Receiving a CMPCTBLOCK:
- The header goes through the §12.3 header rules first.
- If its parent is not an active-chain block, the header is treated as an announcement and the body is downloaded normally.
- Otherwise the receiver fills the slots from its mempool.
- It requests the remaining indices with GETBLOCKTXN (strictly increasing, below the count).
- The responder answers BLOCKTXN with exactly those transactions, in order.
If two candidates match one short ID, a short ID repeats, the transaction counts are inconsistent, or the reconstructed block fails its merkle or witness root, the receiver falls back to GETDATA for the full block without penalising the sender (a short-ID collision is not misbehaviour). A BLOCKTXN with the wrong number of transactions scores 10; GETBLOCKTXN indices out of range score 10.
12.5 Light-client messages
| Type | Name | Body |
|---|---|---|
| 19 | GETCFILTERS | start_height u32 ‖ stop_hash 32 (≤ 1,000 blocks) |
| 20 | CFILTER | block_hash 32 ‖ varbytes filter (one per block, in height order) |
| 21 | GETCFHEADERS | start_height u32 ‖ stop_hash 32 (≤ 2,000 blocks) |
| 22 | CFHEADERS | stop_hash 32 ‖ prev_header 32 ‖ varint n × filter_hash 32 |
Nodes that serve filters set services bit 2 (NODE_FILTERS) in VERSION. A light client:
- syncs headers (§12.3);
- fetches filter headers from several peers and compares them;
- downloads filters and checks each against its header;
- fetches the full blocks that match its locks or assets.
It can verify any filter header against proof of work through the coinbase commitment (§9.2
rule 6), using the coinbase and its merkle branch (REST /v1/blocks/{id}/filter).
12.6 Hardening (policy)
These follow Bitcoin Core:
- Inbound eviction: when inbound slots are full, the node protects 4 peers by lowest latency, 4 that most recently relayed a new block and 4 longest connected. It then evicts the youngest peer of the most represented network group (/16 IPv4, /32 IPv6). If every peer is protected, the new connection is refused.
- ADDR rate limit: a token bucket per peer, 0.1 address per second, burst 1000. Addresses beyond it are ignored.
- Anchors: up to 2 outbound peers are saved to
anchors.jsonand reconnected first after a restart.