Rebuild SPORE from scratch
This is the "reimplement it in any language" guide. It describes the wire format
and the cryptographic identity precisely enough that you can write a compatible
node in C, Python, Go, JavaScript — anything — using only this page and standard
crypto libraries. Every hex value below is generated by the reference code
(cargo run --example worked), so it's authoritative: reproduce these bytes and
you are wire-compatible.
The one-page protocol spec is Spec; this guide walks the bytes by hand. For keeping a running node alive rather than reimplementing the protocol, see Continuity, which also indexes how the pieces fit together.
The primitives you need
SPORE uses five standard building blocks. Any conformant library will do — the algorithms are what matter, not the implementation.
| Purpose | Algorithm | Reference |
|---|---|---|
| Signatures / identity | Ed25519 | RFC 8032 |
| Key agreement (sealed boxes, ratchet) | X25519 | RFC 7748 |
| Sealed-box AEAD | XSalsa20-Poly1305 (NaCl crypto_box) |
NaCl / libsodium |
| Symmetric AEAD (topics, sessions) | ChaCha20-Poly1305 / XChaCha20-Poly1305 | RFC 8439 |
| Hashing (IDs, addresses, fragment selection) | SHA-256 | FIPS 180-4 |
| Ratchet KDF | BLAKE2b | RFC 7693 |
A node that only needs to receive, verify, and display public messages needs just Ed25519 and SHA-256 — everything in sections 1–4 below.
flowchart LR SEED["32-byte seed"] --> SK["Ed25519 secret key"] --> PK["public key"] PK --> ADDR["address = SHA-256(pubkey) first 8 bytes"] TOPIC["a topic string"] --> TADDR["topic = SHA-256(string) first 8 bytes"] ADDR --> DEST["dest field: the two are indistinguishable on the wire, deliberately"] TADDR --> DEST DEST --> ENV["envelope"] PAY["payload"] --> ENV ENV --> WIRE["wire bytes"] WIRE --> ID["id = SHA-256(wire, hops zeroed) first 16 bytes"] WIRE --> SIG["signature = Ed25519 over the same pre-image"] SIG --> ENV
Everything below is that diagram in bytes. Work down it and you have a node.
1. Identity — addresses are hashes of keys
A node's address is the first 8 bytes of the SHA-256 of its Ed25519 public key. There is no registry: the address is the key's fingerprint, so it's self-authenticating.
signing seed : 0707070707070707070707070707070707070707070707070707070707070707
public key (32): ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c
SHA-256(pubkey): fe812c12f3ab4ce6ac5db69ac352f906cb1b11ef43fb33e252ef7ff552263889
address [..8] : fe812c12f3ab4ce6
Topics live in the same 8-byte space: a topic address is SHA-256(name)[..8].
So "news" → 19fba0e995b9794f. A message is "public" when its destination is the
all-zero address 0000000000000000.
2. The envelope — the one wire format
Everything on the wire is an envelope: a small header, an optional source key, the payload, and an optional signature. All multi-byte integers are big-endian.
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 1 | ver |
always 0x02 |
| 1 | 1 | typ |
0=DATA, 1=INV, 2=WANT, 3=ANNOUNCE |
| 2 | 1 | flags |
bitfield, below |
| 3 | 1 | hops |
TTL; decremented by each relay |
| 4 | 4 | created_at |
unix seconds, big-endian — when it was minted, not when it dies |
| 8 | 8 | dest |
destination address (all-zero = public) |
| 16 | 32 or 8 or 0 | src |
present only if SIGNED: 32-byte key, or 8-byte address if SRC8 |
| … | 2 | plen |
payload length, big-endian |
| … | plen |
payload |
the bytes |
| … | 64 or 0 | sig |
present only if SIGNED |
Flag bits: 0x01 ENCRYPTED · 0x02 SIGNED · 0x04 FRAGMENT (retired) ·
0x08 ACKREQ · 0x10 FLOOD · 0x20 SRC8 · 0x40 RATCHET · 0x80 CANCEL.
Nothing expires. created_at says when the envelope was made and that is all
it says. A node keeps what it is given until it needs the room, and only then
does age decide what goes — so how long a message survives is a property of
whoever is carrying it, not of the message. The one rule you must implement is
that an envelope claiming to be from the future (more than a few minutes ahead of
your clock) is refused outright.
A public, unsigned DATA message to "news" carrying "the dam holds":
ver typ flags hops created_at dest plen payload
02 00 10 10 6553f100 19fba0e995b9794f 000d 7468652064616d20686f6c6473
full wire: 020010106553f10019fba0e995b9794f000d7468652064616d20686f6c6473
(flags=0x10 is FLOOD; hops=0x10 is 16; 000d is 13, the payload length.)
3. Message ID — content addressing
An envelope's ID is SHA-256(envelope with the hops byte set to 0)[..16], taken
over the full bytes including the signature if present. Zeroing hops before
hashing is what keeps the ID stable while relays decrement the TTL. For the
envelope above:
id = b0862c14c3be84bc5df1bfa8ab5adacb
The ID is used for dedup (drop an ID you've seen), for naming an envelope in INV/WANT, and for the priority stamp (§10: the stamp is the count of leading zero bits of the ID — free proof-of-work).
It is not what names a file's parts — see Content ID, next.
3b. Content ID — what names a chunk
An envelope ID hashes the whole envelope, so it covers created_at and dest. That
is right for a message and wrong for bytes: the same chunk published a second
later is a different envelope, so a file published twice would share nothing with
itself — which is what happened before this existed.
So the file layer uses a second, narrower name. A content ID is
SHA-256(payload)[..16] — the payload alone, nothing about the message carrying
it:
chunk payload = [CHUNK_TAG] || bytes CHUNK_TAG = 0x07
= 077468652064616d20686f6c6473
content id = 17679394b9bac1cf0bdea0ef71815f51
There is no file id and no chunk index in a chunk payload, and that absence is the point: either would make identical bytes produce different names. The manifest already says which chunks are this file and in what order, and a sealed chunk's AEAD nonce is its position in that order, which the reader counts while walking the tree.
Chunks are a static 4096 bytes — every chunk but the last — the same on every medium, so two publishers cut a file identically and their chunks interchange. Do not derive this from an MTU.
Chunk vs fragment
Two different things, at two different layers. Getting them confused is what caused the bug content IDs exist to fix, so:
| chunk | fragment | |
|---|---|---|
| layer | file (Part III) | link (Part II) |
| size | static, 4096 B, everywhere | one hop's MTU |
| named by | content ID | nothing — it is not an object |
| carries | a slice of one file | any envelope too big for that hop |
| put together by | the fetcher, using the manifest | the far end of that one link |
| lives on the mesh? | yes, it is an envelope | no, never leaves the link |
A chunk is therefore routinely larger than a frame, and a bridge splits it like anything else. That makes per-hop fragmentation a requirement for moving files, not an optimisation.
4. Signing and verifying
To sign: set the SIGNED flag, put your 32-byte public key in src, and compute
Ed25519.sign(secret_key, preimage) where preimage = the envelope body with
hops=0 and no signature yet. The 64-byte signature is appended last. To verify,
recompute the same preimage and check it against the src key.
preimage : 010012006553f10019fba0e995b9794fea4a6c63e29c520abef5507b132ec5f99
54776aebebe7b92421eea691446d22c000d7468652064616d20686f6c6473
signature: daa7ab3bd3c46dda41fd7d95800b91e242f95e43185e4cd1f394bfda7b00cac8
065ecb4c63af711aa2462b950a933215a3234c6ef6b14fc55d495d179cdf3906
(Note the preimage's third byte is 0x12 = FLOOD|SIGNED, and its fourth byte —
hops — is 0x00.)
5. Text armor — the paper/voice channel
Any envelope has a text form that survives SMS, handwriting, a read-aloud phone call, or a photograph: `~S1.AIABEEDFKPYQAGP3UDUZLOLZJ7VEU3DD4KOFECV66VIHWEZOYX4ZKR3WV27L464SIIPOU2IUI3 JCYAANORUGKIDEMFWSA2DPNRSHGMXDZK6UE2IVL4SHPBZ7QTI6GRRS3HSWK5H3OBNJSUHKPMNDZIPX K7RNX6BDM7LUHQAQO5YPVJBKQT2PLNGNBFTN632K2U5NVDPITABA.22DHXIY~
## Build it in tiers
You don't need the whole system to be useful — each tier is a working node.
- **Tier 0 — read & verify.** Parse the envelope (§2), recompute the ID (§3), verify
the signature (§4). You can now receive and trust public messages. Needs only
Ed25519 + SHA-256. Small enough to hand-type.
- **Tier 1 — originate.** Build and sign your own envelopes; encode armor (§5). You
can now send.
- **Tier 2 — relay.** Keep a `seen` set of IDs, drop duplicates and post-dated
envelopes, decrement `hops`, and re-broadcast. You are now a router node. The
flood/dedup/store rules are in [Spec](SPEC.md) §4–§6.
- **Beyond.** Fragmentation (§3 of the spec, a GF(2) fountain code), sealed boxes,
the Double Ratchet, encrypted topics, sessions, files, and mix mode are all
layered on the same envelope — add them as you need them.
## Conformance
A reimplementation is wire-compatible if it reproduces the hex in sections 1–4
exactly. Run `cargo run --example worked` in this repo to regenerate the reference
values (they're deterministic — fixed key seeds), and diff against your output.