§ 12

§ 12 · Networking

Encrypted transport

Every connection encrypted, with no way to switch it off

When two Pearl nodes talk, nobody in between can read what they say. Every connection is encrypted with BIP-324, a standard written for Bitcoin, and Pearl made it the only option. There’s no plaintext mode to fall back to and no setting to turn encryption off.

The idea is the same one that protects web pages: the two sides agree on a secret key without ever sending it, then encrypt everything with it. What’s special about BIP-324 is that even the very first bytes look random, so a watcher can’t tell the conversation is Pearl at all.

Try it

Encryption keys here are derived with hash functions. Type a message and watch a hash change completely when one character changes: that’s the property that keeps derived keys unguessable.

Hash playground

Change one letter and the whole fingerprint changes

SHA-256 Tagged hashes and Taproot

72bc556112e9538a2433f0a9b87b032eae43c69e367e9dca1b176929dcf20ed5

Avalanche SHA-256 · 256 bits

135 of 256 bits changed

compared with “pearl”

Runs in your browser · nothing is sent src/lib/hash.ts · raw output order

How does the handshake work?

In four steps, all from BIP-324, with Pearl’s salt in step 3:

  1. Swap disguised public keys

    Each node makes a fresh key pair on secp256k1, Bitcoin’s curve, and sends its public key as 64 bytes encoded with ElligatorSwift. Unlike a normal public key, those 64 bytes are indistinguishable from random noise. Each side may add up to 4,095 bytes of random garbage after its key, so even the length of the opening says nothing.

  2. Compute a shared secret

    Each node combines its own private key with the other’s public key (elliptic-curve Diffie-Hellman). Both get the same secret; nobody watching can.

  3. Expand it into keys

    HKDF with SHA-256 turns the secret into separate keys for each direction, plus a session ID and the “garbage terminators” that mark where the garbage ends. Pearl’s salt for this step is pearl_v2_shared_secret followed by the network’s magic bytes.

  4. Confirm and talk

    Each side sends its garbage terminator and an encrypted version packet. From then on, every message, starting with the ordinary version and verack, travels encrypted.

What is Pearl’s salt?

For mainnet, 26 bytes: the ASCII text pearl_v2_shared_secret, then the magic 0x50524C4D written little-endian.

BIP-324 HKDF salt, mainnet 26 bytes
706561726c5f76325f7368617265645f736563726574 4d4c5250
Field Bytes Value
"pearl_v2_shared_secret" 22 @0 22 bytes of ASCII text
Magic, little-endian 4 @22 0x50524C4D, "PRLM"
Bitcoin's salt is bitcoin_v2_shared_secret followed by its own magic, f9 be b4 d9.

That small change does two jobs. A Pearl node and a Bitcoin node derive different keys and can’t talk by accident. And because the magic is in the salt, Pearl’s own networks are kept apart the same way: a testnet node’s salt ends 31 4c 52 50, so its keys never match a mainnet node’s. More on that is on magic bytes and ports.

What does an encrypted message look like?

From the outside, a stream of random bytes. Inside, each message is a packet:

A BIP-324 packet
Field Size Description
Length 3 How long the contents are, encrypted with its own ChaCha20 key so even message sizes aren’t sent in the clear.
Header byte 1 A flag byte, encrypted with the contents (it can mark a packet as decoy, to be ignored).
Contents varies The message: a one-byte message ID (or 00 and a 12-byte name) and its payload, encrypted with ChaCha20-Poly1305.
Tag 16 Poly1305 authentication tag. Any tampering makes it fail and the connection drop.

Both ciphers roll their keys forward every 224 messages, so a key stolen later can’t decrypt older traffic in that session. The message IDs and payloads inside are described on messages.

Why does Pearl insist on it?

Pearl’s code doesn’t give a reason; it simply has no other path. Both incoming and outgoing connections start with the BIP-324 handshake, every message is read and written in the encrypted form, and every node advertises the NODE_P2P_V2 service bit by default.

The effects are easy to see, though:

  • No magic bytes on the wire. Nothing in the stream says “Pearl”. The magic only appears in the salt and in block files on disk.
  • Privacy from passive watchers. An internet provider can’t read which transactions or blocks your node relays.
  • Tamper-evidence. Any byte changed in transit breaks the authentication tag.
  • One protocol. No downgrade to plaintext for an attacker to force.

Make a guess

Pearl's encryption keys depend on the network magic. If a future Pearl testnet3 picks magic 0x50524C33, can its nodes accidentally join mainnet?

Think of your answer first, then check it.

Answer No. Their salt would end 33 4c 52 50, so their keys would never match mainnet's.

The key exchange itself would work, but each side would derive different keys from its own salt. Neither would find the garbage terminator it expects or be able to decrypt the other’s first packet, so the connection would drop before any block or transaction was exchanged.

Code: deriving the keys

From transport.go (node/v2transport/transport.go:186-275 at v1.5.2, on GitHub):

node/v2transport/transport.go Go
salt := []byte("pearl_v2_shared_secret")
var magic [4]byte
binary.LittleEndian.PutUint32(magic[:], uint32(net))
salt = append(salt, magic[:]...)

prk := hkdf.Extract(sha256.New, ecdhSecret, salt)

sessionReader := hkdf.Expand(sha256.New, prk, []byte("session_id"))
// … then "initiator_L", "initiator_P", "responder_L", "responder_P"
// and "garbage_terminators", each its own HKDF-Expand.

Inbound connections answer the handshake and outbound ones start it, in peer.go (node/peer/peer.go:2235-2300 at v1.5.2, on GitHub); in both cases the v2 handshake runs before the version message.

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.