§ 12

§ 12 · Networking

Messages

What nodes say to each other, and why headers come 100 at a time

Once two nodes are connected, they talk in short, typed messages: “here’s a new block”, “send me that transaction”, “what headers do you have after this one?”. Pearl’s vocabulary is btcd’s: Bitcoin’s, word for word, minus the compact-block messages. No new message types were added.

What changed is what some messages carry. Pearl blocks travel behind a ~60 KB certificate, and nodes check those certificates while syncing, so the messages that carry headers had to be redesigned around them.

How is a message packaged?

Every message travels inside a BIP-324 encrypted packet. Inside the encryption, a message is just two things:

  1. Which message it is. Usually one byte, a short ID: 2 is block, 12 is getheaders, 13 is headers, 14 is inv, 21 is tx. A few rarer messages use a 00 byte followed by their name spelled out in 12 bytes: version, verack, getaddr, sendheaders, sendaddrv2, wtxidrelay, reject.
  2. The payload, in the same byte format as Bitcoin’s, except for the messages below.

Bitcoin’s older v1 header, with magic bytes, a command name, a length and a checksum, is never used: pearld’s peer code always reads and writes the encrypted v2 form.

How does a conversation start?

Right after the encrypted handshake, each side sends a version message: its protocol version (2), its services, its user agent. Each answers the other’s with verack. That’s the version handshake.

pearld refuses peers whose protocol version is below 2. The protocol also has a few optional preference messages: sendheaders (announce new blocks as headers), sendaddrv2 (use the newer address format), wtxidrelay (name transactions by wtxid), feefilter (don’t send me transactions cheaper than this). pearld itself sends only sendaddrv2 during the handshake, but it understands the others.

How do new blocks and transactions spread?

By announcement, so nobody receives the same data twice:

  1. Announce

    A node that has something new sends an inv listing hashes: “I have block X, transactions Y and Z.”

  2. Ask

    A peer that lacks one replies with getdata for those hashes.

  3. Send

    The node sends the block or tx. If it no longer has it, notfound.

To peers that asked for it with sendheaders, pearld announces a new block as a one-entry headers message instead, and that announcement carries the block’s certificate. (pearld never asks for this itself, so between pearld nodes new blocks are announced with inv.)

There are no compact blocks. Bitcoin’s BIP-152 lets a node send a block as short transaction IDs that the peer rebuilds from its own mempool. Pearl’s message table simply leaves those IDs out (sendcmpct, cmpctblock, getblocktxn, blocktxn), so blocks always travel whole.

How does a new node download the chain?

Headers first, with certificates, a checkpoint at a time. This is headers-first sync.

The node sends getheaders. Its payload has Bitcoin’s fields, plus one more byte at the end:

getheaders
Field Size Type Description
Protocol version 4 uint32 LE 2
Locator count 1+ CompactSize How many hashes follow.
Block locator 32 each hashes Hashes from the node’s own chain, newest first, getting sparser further back, so the peer can find where the chains split.
Hash stop 32 hash Stop here (all zeros: send as many as allowed).
Include certificates 1 bool New in Pearl. 00 = headers only; anything else = include each header’s certificate.

Here is a real-format getheaders, built with Pearl’s own wire code (not captured from the network): “I only have genesis; send what comes next, with certificates.” 70 bytes:

getheaders payload 70 bytes
02000000 01 d7ad8dc5415fb548ff53efe75f10e0e27403d1a33f07dd0c8f61ffb793308da1 0000000000000000000000000000000000000000000000000000000000000000 01
Field Bytes Value
Protocol version 4 @0 2
Locator count 1 @4 1
Locator: genesis hash 32 @5 a18d3093…c58dadd7 reversed
Hash stop 32 @37 none
Include certificates 1 @69 yes
Protocol version 2, one locator (the genesis hash, stored byte-reversed), no stop hash, includeCertificates = 1.

The peer answers with a headers message: a count, then up to 100 entries, each a certificate followed by a 108-byte header. Without certificates, each entry starts with 00000000, the empty “version 0” certificate. Here is a one-header reply carrying the genesis header without its certificate, 113 bytes:

headers payload (no certificates) 113 bytes
01 00000000 010000000000000000000000000000000000000000000000000000000000000000000000c1751637382fbd52f5e4e26dc9f38d8ccc2ab216dfed38bd489623f1c6592f309025ef69ffff001b0000000000000000000000000000000000000000000000000000000000000000
Field Bytes Value
Count 1 @0 1 entry
Certificate 4 @1 version 0: none
Genesis header 108 @5 108 bytes
Count 1, an empty certificate, then the 108-byte genesis header. Unlike Bitcoin, no transaction-count byte follows the header.

With the genesis block’s (placeholder) certificate included, the same reply is 317 bytes. With real ones it’s much bigger:

headers message Size
100 entries with today’s certificates, 59,768 bytes each about 5,987,601 bytes (6.0 MB)
100 entries with maximum-size certificates, 65,000 bytes each about 6,510,801 bytes (6.5 MB)
Any message, hard cap 8,000,000 bytes
Bitcoin, for comparison: 2,000 headers × 81 bytes about 162 KB

That’s why the cap is 100, not Bitcoin’s 2,000. A single headers message can’t mix entries with and without certificates.

What does a syncing node do with each header?

It verifies it, fully, before asking for the next batch:

  1. The header must link to the previous one; otherwise the peer is disconnected.
  2. The certificate must follow the rules for that height: right version, dense-only, rank penalty.
  3. The header and certificate must pass the sanity checks, including the full zero-knowledge proof.

This repeats up to the next checkpoint; then the node fetches the blocks. Past the last checkpoint it switches to ordinary inv and getdata. Two safety rules round it off. Outside headers-first sync, a peer that sends headers carrying certificates is disconnected. And when a low-quality peer announces a block, the node first probes with a cheap certificate-less getheaders, and only fetches the block if the answer checks out.

What does a block message look like?

Exactly like a block on disk, certificate first:

𝚋𝚕𝚘𝚌𝚔=certificate‖header108‖tx count‖transactions\texttt{block} = \text{certificate} \;\|\; \text{header}_{108} \;\|\; \text{tx count} \;\|\; \text{transactions}

The header and transactions may total 4,000,000 bytes; with a certificate of up to 65,000 bytes in front, a block message tops out at 4,065,000 bytes. The full layout, byte by byte, is on blocks.

merkleblock, the reply to a BIP-37 bloom-filter client, carries the 108-byte header without its certificate. A client fed only merkleblocks can’t check the proof of work for itself. Pearl’s own light client uses compact block filters and fetches headers with certificates instead.

Make a guess

A Bitcoin-trained developer parses a Pearl headers message by reading 81 bytes per header. Where does it go wrong first?

Think of your answer first, then check it.

Answer Right after the count: the first four bytes are a certificate version, not the header's version.

Each Pearl entry starts with a certificate (or 00000000), then a 108-byte header, with no trailing transaction-count byte. Even with certificates off, every entry is 4 + 108 = 112 bytes, not 81. A parser written for Bitcoin misreads everything from the first entry on.

Every message and its BIP-324 ID

From message.go (node/wire/message.go:71-96 at v1.5.2, on GitHub). Messages without a one-byte ID are sent as 00 plus their 12-byte name.

ID Message ID Message
1 addr 16 merkleblock
2 block 17 notfound
5 feefilter 18 ping
6 filteradd 19 pong
7 filterclear 21 tx
8 filterload 22 getcfilters
9 getblocks 23 cfilter
11 getdata 24 getcfheaders
12 getheaders 25 cfheaders
13 headers 26 getcfcheckpt
14 inv 27 cfcheckpt
15 mempool 28 addrv2

Long form: version, verack, getaddr, sendheaders, sendaddrv2, wtxidrelay, reject. Missing, because btcd never implemented compact blocks: 3 blocktxn, 4 cmpctblock, 10 getblocktxn, 20 sendcmpct.

The headers entry type, from msgheaders.go (node/wire/msgheaders.go:10-41 at v1.5.2, on GitHub):

node/wire/msgheaders.go Go
// 2000 is too many for our Block Header size, so we limit it to 100.
const MaxBlockHeadersPerMsg = 100

type MsgHeader struct {
	MsgCertificate MsgCertificate
	BlockHeader    BlockHeader
}

The codex

Suggested

Up and down arrows move through the results, Enter opens one, Escape closes. Anywhere on the site, slash or Command K opens this search, and the square brackets [ and ] go to the previous and next page.