§ 12 · Networking
Encrypted transport
Every connection encrypted, with no way to switch it off
On this page 5 sections
When two Pearl nodes talk, nobody in between can read what they say. Every connection is encrypted with BIP-324BIP-324: A standard for encrypting the connection between two nodes. Pearl uses it for every connection, with no unencrypted fallback., 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.
How does the handshake work?
In four steps, all from BIP-324, with Pearl’s salt in step 3:
-
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 ElligatorSwiftElligatorSwift: A way of writing an elliptic-curve public key as 64 bytes that look completely random. BIP-324 uses it so even the key exchange that opens a connection gives nothing away.. 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.
-
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.
-
Expand it into keys
HKDFHKDF: A standard recipe, built on HMAC, for turning one shared secret into several independent keys. Pearl’s encrypted transport salts it with "pearl_v2_shared_secret" plus the network’s magic bytes. 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_secretfollowed by the network’s magic bytes. -
Confirm and talk
Each side sends its garbage terminator and an encrypted version packet. From then on, every message, starting with the ordinary
versionandverack, 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.
| Field | Bytes | Value |
|---|---|---|
| "pearl_v2_shared_secret" | 22 @0 | 22 bytes of ASCII text |
| Magic, little-endian | 4 @22 | 0x50524C4D, "PRLM" |
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:
| 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):
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.