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.