Skip to main content

Wire Formats

This page catalogs every binary layout, serialization format, and encoding convention used in the SDK.

Ratchet message payload

Every post-handshake message is a single binary blob emitted as an Ethereum event. The signature comes first so recipients can verify authenticity before touching any ratchet state. See ratchet/codec.ts.

Offset  Size     Field
──────────────────────────────────────────────────
0 1 Version (0x01)
1 64 Ed25519 signature (detached)
65 32 DH ratchet public key
97 4 pn (uint32, big-endian)
101 4 n (uint32, big-endian)
105 24 XSalsa20-Poly1305 nonce
129 var XSalsa20-Poly1305 ciphertext
──────────────────────────────────────────────────
Minimum total: 129 + ciphertext bytes

The signature covers bytes 65 through the end (header + nonce + ciphertext). Verification is Ed25519.verify(signature, payload[65..], senderSigningPublicKey).

pn is the number of messages sent in the previous DH epoch (used by the receiver to pre-derive skipped keys). n is the message number in the current epoch.

Unified public key format

Long-term public keys are encoded as a versioned 65-byte blob. Used in the pubKeys field of the Handshake event. See payload.ts.

Byte 0        Bytes 1..32       Bytes 33..64
┌────────┬──────────────────┬──────────────────┐
│ 0x01 │ X25519 (32 B) │ Ed25519 (32 B) │
└────────┴──────────────────┴──────────────────┘

Extended ephemeral key format

The ephemeralPubKey field in the Handshake event concatenates the X25519 ephemeral key with the ML-KEM-768 public key. See handshake.ts.

Bytes 0..31          Bytes 32..1215
┌────────────────┬─────────────────────────┐
│ X25519 (32 B) │ ML-KEM-768 pk (1184 B) │
└────────────────┴─────────────────────────┘
Total: 1216 bytes

The responder splits on offset 32 to extract both keys.

Handshake plaintext payload

The plaintextPayload field in the Handshake event is a UTF-8 encoded JSON string. It is not encrypted because the identity proof is intended to be publicly verifiable. See payload.ts.

{
"plaintextPayload": "Hello, want to chat?",
"identityProof": {
"message": "VerbEth Key Binding v1\nAddress: 0x...\n...",
"signature": "0x..."
}
}

Handshake response encrypted payload

The ciphertext field in the HandshakeResponse event is a UTF-8 encoded JSON envelope wrapping a NaCl box. The box is encrypted with X25519 using the ratchet keypair (not the tag keypair). See crypto.ts and payload.ts.

Envelope format

{
"v": 1,
"epk": "<base64 ephemeral public key>",
"n": "<base64 nonce (24 bytes)>",
"ct": "<base64 NaCl box ciphertext>",
"sig": "<base64 Ed25519 signature (optional)>"
}

Decrypted inner content

After NaCl.box.open(ct, n, epk, initiatorEphemeralSecret), the resulting JSON contains:

{
"unifiedPubKeys": "<base64, 65 bytes>",
"ephemeralPubKey": "<base64, 32 bytes (ratchet DH key)>",
"kemCiphertext": "<base64, 1088 bytes (ML-KEM ciphertext)>",
"note": "Hey, accepted!",
"identityProof": { ... }
}

The ephemeralPubKey here is the responder's ratchet DH public key (not the tag key R that appears on-chain). The kemCiphertext is the ML-KEM-768 encapsulation that the initiator decapsulates to recover the KEM shared secret.

Ciphertext padding

Before encryption, plaintext is framed and padded to reduce length-based metadata leakage. See ratchet/padding.ts.

┌────────┬──────────────────────┬───────────┬──────────────────┐
│ 0x00 │ plaintext length │ plaintext │ random padding │
│ marker │ (4 bytes, big-endian)│ │ │
└────────┴──────────────────────┴───────────┴──────────────────┘

Bucket selection follows this scheme.

Framed sizeBucket rule
≤ 64 bytes64 (minimum)
65 to 16,384 bytesNext power of 2
> 16,384 bytesNext multiple of 4,096

On top of the bucket size, a random jitter of up to floor(bucket / 8) bytes is added using nacl.randomBytes. The padding bytes are cryptographically random and become indistinguishable from ciphertext after XSalsa20-Poly1305 encryption. The overall effect is that ciphertext sizes reveal at most O(log n) bits about plaintext length in the exponential range.

Topic format

Topics are bytes32 values derived from HKDF output and then hashed with keccak256. See ratchet/kdf.ts.

okm    = HKDF-SHA256(ikm=dhOutput, salt=rootKey, info="verbeth:topic-{direction}:v3", len=32)
topic = keccak256(okm)

Recipient hash

The recipientHash field in the Handshake event identifies the intended recipient without revealing their address directly. See handshake.ts.

recipientHash = keccak256(utf8("contact:" + lowercase(recipientAddress)))

Identity proof binding message

The structure is defined in Identity. For wire format reference, the message is a newline-delimited plaintext string.

VerbEth Key Binding v1
Address: 0xabc...
PkEd25519: 0x123...
PkX25519: 0x456...
ExecutorAddress: 0xdef...
ChainId: 8453
RpId: my-app

This message is ECDSA-signed by the wallet and included in the identityProof field of both handshake and handshake response payloads.