§ 12 · Networking
Messages
What nodes say to each other, and why headers come 100 at a time
On this page 5 sections
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:
- Which message it is. Usually one byte, a short ID:
2isblock,12isgetheaders,13isheaders,14isinv,21istx. A few rarer messages use a00byte followed by their name spelled out in 12 bytes:version,verack,getaddr,sendheaders,sendaddrv2,wtxidrelay,reject. - 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 handshakeVersion handshake: The first two messages two nodes swap after connecting: each sends a version message (protocol version, services, user agent) and answers the other’s with verack..
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:
-
Announce
A node that has something new sends an
invinv message: A short announcement listing the hashes of new blocks or transactions a node has. A peer that lacks one asks for it with getdata. listing hashes: “I have block X, transactions Y and Z.” -
Ask
A peer that lacks one replies with
getdatafor those hashes. -
Send
The node sends the
blockortx. 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 syncHeaders-first sync: How a new node catches up: it first downloads and checks block headers up to the next checkpoint, then fetches the blocks. On Pearl each header comes with its certificate, and every certificate is verified..
The node sends getheaders. Its payload has Bitcoin’s fields, plus one more byte at the end:
| 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:
| 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 |
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:
| Field | Bytes | Value |
|---|---|---|
| Count | 1 @0 | 1 entry |
| Certificate | 4 @1 | version 0: none |
| Genesis header | 108 @5 | 108 bytes |
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:
- The header must link to the previous one; otherwise the peer is disconnected.
- The certificate must follow the rules for that height: right version, dense-only, rank penalty.
- 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:
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):
// 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
}