End-to-end encrypted node communications using X25519 key exchange and ChaCha20-Poly1305 AEAD. Every peer connection is authenticated and encrypted by default. Port 19333, magic 0x534F5354 ("SOST").
All peer-to-peer encryption uses modern, well-audited primitives. Each connection generates fresh ephemeral keys — no long-term key material is reused across sessions.
256-bit ephemeral Curve25519 keypairs generated per connection. Each side produces a 32-byte public key and derives a 32-byte shared secret via Diffie-Hellman. The ephemeral nature ensures forward secrecy — compromising one session reveals nothing about past or future sessions.
Authenticated Encryption with Associated Data. 256-bit key, 96-bit nonce (64-bit counter + 32-bit zero padding), 16-byte authentication tag per message. Provides both confidentiality and integrity — any tampering is detected and the message is rejected.
The shared secret from X25519 is never used directly. HKDF-SHA256 derives separate send and receive keys using distinct labels:
send_key = HKDF-SHA256(shared_secret, label="sost-p2p-key-a") recv_key = HKDF-SHA256(shared_secret, label="sost-p2p-key-b")
The initiator uses key-a to send and key-b to receive; the responder reverses this. Each direction has an independent 64-bit nonce counter starting at 0, incremented per message.
Node operators can configure their encryption policy via the --encryption flag. Three modes are supported:
| Mode | Flag | Behavior |
|---|---|---|
| off | --encryption off | No encryption. Messages sent in plaintext. EKEY exchange is skipped. Useful for debugging or trusted LAN environments only. |
| on (default) | --encryption on | Encryption enabled. Node sends EKEY and encrypts if the peer reciprocates. Falls back to plaintext if the peer does not support encryption. Recommended for most deployments. |
| required | --encryption required | Encryption mandatory. Node disconnects any peer that does not complete the EKEY handshake. Maximum security — no plaintext connections allowed. |
off mode exists for development and diagnostics only.The connection handshake establishes encryption before any protocol data is exchanged. The EKEY exchange happens first, then VERS/VACK, then all subsequent messages are encrypted.
Initiator Responder
───────── ─────────
│ │
│──── EKEY (pubkey_a, 32B) ───────>│
│ │
│<─── EKEY (pubkey_b, 32B) ────────│
│ │
│ [Both derive shared_secret via X25519]
│ [HKDF-SHA256 → send_key, recv_key]
│ │
│════════ ENCRYPTED CHANNEL ═══════│
│ │
│──── VERS (version, height) ─────>│
│ │
│<─── VACK (version, height) ──────│
│ │
│══ HANDSHAKE COMPLETE ════════════│
│ │
│ [GETB, BLCK, TXXX, PING/PONG] │
│ [All messages encrypted] │
│ │
Timeout: If EKEY is not received within 10 seconds of connection, the peer is disconnected. If VERS/VACK is not completed within 30 seconds, the peer is disconnected and scored for misbehavior.
All commands use the same 12-byte frame header. Command names are 4-byte ASCII identifiers padded with null bytes if shorter.
| Command | Direction | Payload | Purpose |
|---|---|---|---|
| EKEY | Both | 32-byte X25519 public key | Exchange ephemeral encryption keys. Must be the first message on a new connection. |
| ENCR | Both | 1-byte mode flag | Declare encryption capability and mode. Sent alongside or after EKEY. |
| VERS | Initiator → Responder | Protocol version (u32), best height (u64), genesis hash (32B) | Version handshake. Initiator announces its chain state. |
| VACK | Responder → Initiator | Protocol version (u32), best height (u64), genesis hash (32B) | Version acknowledgement. Responder confirms compatibility. |
| GETB | Either | Start height (u64), count (u16) | Request a batch of blocks starting from the given height. Max 100 blocks per request. |
| BLCK | Either | Serialized block data | Deliver a single block in response to GETB, or relay a newly mined block. |
| DONE | Either | None (0 bytes) | Signal end of a GETB batch. Sender has no more blocks to send for this request. |
| TXXX | Either | Serialized transaction | Relay an unconfirmed transaction for mempool inclusion. |
| PING | Either | 8-byte nonce | Keepalive probe. Peer must respond with PONG containing the same nonce. |
| PONG | Either | 8-byte nonce (echo) | Keepalive response. Nonce must match the corresponding PING. |
When a node discovers a peer with a higher best height, it initiates block synchronization. Blocks are requested and delivered in batches of up to 100.
Node (behind) Peer (ahead)
───────────── ────────────
│ │
│ [VERS/VACK: peer height > ours]│
│ │
│── GETB (height=1000, n=100) ────>│
│ │
│<──── BLCK (block 1000) ──────────│
│<──── BLCK (block 1001) ──────────│
│<──── BLCK (block 1002) ──────────│
│ ... │
│<──── BLCK (block 1099) ──────────│
│<──── DONE ──────────────────────│
│ │
│ [Validate & connect blocks] │
│ │
│── GETB (height=1100, n=100) ────>│
│ │
│<──── BLCK (block 1100) ──────────│
│ ... │
│<──── BLCK (block 1155) ──────────│
│<──── DONE ──────────────────────│
│ │
│ [Sync complete: heights match] │
│ │
| Parameter | Value | Description |
|---|---|---|
| Batch size | 100 blocks max | Maximum blocks per GETB request |
| Timeout | 30 seconds | Per-batch timeout. If no BLCK/DONE received within 30s, retry or disconnect. |
| Retries | 3 | Maximum retry attempts per batch before disconnecting the peer. |
| Rate (sync) | 5,000 blocks/min | Elevated rate limit during active sync to allow fast catch-up. |
| Rate (steady) | 50 blocks/min | Normal rate limit after sync is complete. |
Once the EKEY handshake is complete, all messages are wrapped in an encrypted frame. The frame header is sent in plaintext (so the receiver knows how much ciphertext to read), but the payload is encrypted.
┌──────────────────────────────────────────────────────────┐
│ ENCRYPTED FRAME │
├──────────┬──────────┬─────────────┬─────────────┬────────┤
│ magic │ cmd │ payload_len │ ciphertext │ tag │
│ 4 bytes │ 4 bytes │ 4 bytes │ N bytes │ 16 B │
├──────────┼──────────┼─────────────┼─────────────┼────────┤
│ 0x534F5354│ "BLCK" │ len(ct) │ ChaCha20 │ Poly │
│ "SOST" │ "GETB" │ + 16 │ encrypted │ 1305 │
│ │ "TXXX" │ │ payload │ MAC │
│ │ etc. │ │ │ │
└──────────┴──────────┴─────────────┴─────────────┴────────┘
Header (12 bytes, plaintext):
magic: 0x534F5354 ("SOST") — identifies SOST protocol frames
cmd: 4-byte ASCII command identifier
payload_len: total encrypted payload size including 16-byte auth tag
Encrypted payload:
ciphertext: ChaCha20 encrypted message body (N bytes)
tag: Poly1305 16-byte authentication tag
The payload_len field includes the 16-byte Poly1305 tag, so actual plaintext length = payload_len - 16. Maximum payload_len = 4,194,304 bytes (4 MB). Any frame exceeding this is rejected and the peer is banned.
Each direction maintains an independent 64-bit nonce counter, starting at 0 and incremented after each message. The nonce is not transmitted — both sides track it implicitly. If counters desync (decryption fails), the connection is terminated immediately.
Every P2P message begins with a fixed 12-byte header, whether encrypted or plaintext. This header is always sent unencrypted so the receiver can determine the message type and allocate the correct buffer size.
Offset Size Field Description
────── ──── ───── ───────────
0 4 magic 0x534F5354 ("SOST") — protocol identifier
4 4 cmd ASCII command (e.g., "EKEY", "BLCK", "GETB")
8 4 payload_len Payload size in bytes (network byte order)
────── ────
12 bytes total
| Byte | Hex | ASCII |
|---|---|---|
| 0 | 0x53 | S |
| 1 | 0x4F | O |
| 2 | 0x53 | S |
| 3 | 0x54 | T |
Any frame that does not begin with 0x534F5354 is immediately rejected. This allows quick rejection of non-SOST traffic and port-scanning probes.
Maximum message size: 4 MB (4,194,304 bytes). The payload_len field is a 32-bit unsigned integer but values above 4 MB are rejected at the frame parser level. A peer sending an oversized frame is disconnected and receives ban points.
The P2P layer implements multiple defensive mechanisms to prevent denial-of-service attacks, resource exhaustion, and misbehaving peers.
| Context | Rate | Description |
|---|---|---|
| Steady state | 50 blocks/min | Normal operation after initial sync. Prevents block flooding. |
| Active sync | 5,000 blocks/min | Elevated limit during initial block download to allow fast catch-up. |
| Max message size | 4 MB | Any frame with payload_len > 4,194,304 bytes is rejected immediately. |
| Parameter | Value | Description |
|---|---|---|
| Max inbound | 32 | Maximum simultaneous inbound peer connections. |
| Per-IP limit | 2 | Maximum connections from a single IP address. |
| IP cooldown | 30 seconds | Minimum time between connection attempts from the same IP after disconnect. |
Each peer accumulates misbehavior points for protocol violations. When a peer reaches the ban threshold, it is disconnected and its IP is blocked for the ban duration.
| Parameter | Value |
|---|---|
| Ban threshold | 100 points |
| Ban duration | 24 hours |
Misbehavior examples:
Deal channels now use true end-to-end encryption. The relay is blind transport — it cannot read message content.
| Identity & Signing | ED25519 (permanent, signs everything) |
| Key Agreement | X25519 (per-deal ephemeral key exchange) |
| Key Derivation | HKDF-SHA256 with directional labels |
| Payload Encryption | ChaCha20-Poly1305 AEAD |
| Replay Protection | Sequence numbers + sliding window + nonce dedup |
| Signed Prekey | X25519, rotated every 7 days, signed by ED25519 identity key |
| One-Time Prekeys | Consumed once per session, replenished in batches of 10 |
| Async Bootstrap | Sender initiates encrypted channel while recipient is offline |
| Offline Delivery | Store-and-forward queue, 7-day TTL, delivery receipts |
| CAN | Store, forward, route encrypted envelopes |
| CAN | Verify header signatures for routing authentication |
| CAN | Queue messages for offline recipients |
| CANNOT | Read message payload content |
| CANNOT | Fabricate valid encrypted messages |
| CANNOT | Reuse one-time prekeys without detection |