SPORE v1 — Store-and-forward Planetary Opportunistic Relay Envelope

Front: the protocol. Back: how it rides everything. Two sides of one sheet — the normative whole. Worked bytes for every rule here: Rebuild guide. Per-medium parameters: Bridges.

SPORE v1 one-page visual reference

0. The whole protocol in one breath

A SPORE message is a signed postcard: to, from, expiry, payload, signature. Its SHA-256 fingerprint is its identity. Every node keeps postcards it hasn't seen, hands copies to anyone it meets who wants them, and drops duplicates and expired mail. That alone is a working planetary network. Of the four hard features — forward secrecy, fountain fragmentation, congestion control, anonymity — only congestion control touches the router; the rest live inside payloads.

Tiers (all interoperate): T0 carry ≈60 lines: parse, dedup, store, deliver, damped flood · T1 sync +≈80: ANNOUNCE/INV/WANT, watermarks · T2 route +≈100: paths, directed unicast, custody. Endpoint extras (ratchet, fountain, mix) never change relays.

Threat model, stated once: every link is hostile — logged, spoofed, jammed, MITM'd. Links are trusted with nothing; authenticity and secrecy live only in the envelope. Attackers can drop or delay; redundancy and flood-fallback heal both.

The runtime contract — what the host must supply

The protocol is pure; it holds no OS. A conformant node needs four things from whatever runs it, and gets them wrong silently if it does not ask. Transport is not a fifth: the protocol names interfaces but never opens one, so bytes in and out are the boundary this contract is stated across, not an item on it (Page 2; Design Part 2).

1. Identity & addressing

Identity = one Ed25519 keypair. Address = first 8 B of SHA-256(pubkey). Topic = first 8 B of SHA-256(UTF-8 string). No global namespace; exchange keys by QR/paper/voice. Petnames are local.

2. Envelope (the only object; big-endian; fixed part = 16 B)

off len field
0   1   ver    = 0x01  (layout frozen; forward envelopes with unknown flag bits set)
1   1   type   0=DATA 1=INV 2=WANT 3=ANNOUNCE
2   1   flags  b0 ENCRYPTED b1 SIGNED b2 FRAGMENT b3 ACKREQ b4 FLOOD b5 SRC8
               b6 RATCHET (0x40, §7)   b7 reserved, MUST be 0
3   1   hops   remaining relays (default 16; relays clamp incoming to ≤ 16)
4   4   expiry unix seconds u32 (stores clamp horizon to 30 d)
8   8   dest   address | topic | 0x00×8 = public
-- if SIGNED: src = 32-B pubkey, or 8-B address if SRC8 --
    2   plen   u16
    N   payload
    64  sig    Ed25519 over all bytes above with hops zeroed
-- unsigned: no src, no sig; rides last everywhere --

ID = first 16 B of SHA-256(envelope, hops zeroed); computed, never transmitted (except inside INV/WANT/frag/ack). Overhead: 114 B signed, 90 B SRC8, 18 B unsigned. SRC8 only toward peers that provably hold your key; relays never verify — endpoints do. No priority field: priority is bought, not claimed (§10 stamp).

3. Fragmentation — fountain coded

payload = [orig_id:16][index:1][count:1][chunk]; all chunks equal size (pad the original; the envelope self-delimits). Fragments are ordinary envelopes (own IDs, same dest/expiry).

Receiver decodes when any received set reaches rank count (Gaussian elimination over GF(2)); typically count+2 arrivals suffice at any loss rate, in any order, even one-way. Verify the reassembled signature; commit only what verifies. Rateless: works on simplex radio, CW, paper tape.

4. Routing state (T2)

ANNOUNCE (type 3, signed): payload = [prekey:32][nt:1][topic×8 ea][np:1][(addr:8, age_min:2) ea][petname…] — your current encryption prekey (§7), topics you collect, freshest paths. Link HELLO = hops 0; flooded = hops 16.

5. Forwarding rules (the entire router)

  1. Envelope arrives: ID seen or expired → drop. Add ID (keep ≥ until expiry). Learn paths (§4).
  2. dest ∈ {my addresses, followed topics, 0×8} → deliver (verify/decrypt per flags).
  3. Store until expiry. Evict: expired → lowest stamp → largest → oldest. TX order: local origin, then stamp, then FIFO.
  4. Congestion control, four rules: (a) token bucket — relayed traffic ≤ 10% of each interface's capacity (law on ISM bands, courtesy elsewhere; dedup makes dropped relays harmless); (b) Trickle timers — HELLO/ANNOUNCE interval doubles 5→80 min while nothing new is heard, resets to 5 on any novelty; (c) backpressure — HELLO carries one busy byte (queue fill); neighbors scale sending by (255−busy)/255 and defer unstamped relays to busy peers; (d) exponential backoff — FLOOD retries at 30 s ×2, cap 1 h, max 5.
  5. hops = 0 → stop. Else decrement, then: topic/0/FLOOD → damped flood on all interfaces (on shared media wait random 1–5× airtime, cancel if the ID is overheard ≥ 2×; ≥ 1× for directed) · unicast + fresh path → that interface/neighbor only · unicast, no path → silent, unless you hold custody and the path died: set FLOOD, continue.
  6. Originator: no echo/ACK → resend with FLOOD per 4d. Flooding is route discovery; replies teach reverse paths and heal blackholes.
  7. Untrusted clock? Relay regardless of expiry; age by dwell, drop after 7 local days.
def on_rx(e, iface, nbr):
    if id(e) in seen or expired(e): return
    seen.add(id(e)); store.put(e)
    if e.SIGNED: paths.learn(addr(e.src), iface, nbr)   # first copy wins, keep 3
    if e.dest in my_addrs | topics | {ZERO}: deliver(e)
    if not e.hops: return
    e.hops -= 1
    if unicast(e.dest) and not e.FLOOD:
        p = paths.fresh(e.dest)
        if p: tx(p.iface, p.nbr, e)
        elif held_custody(e): e.FLOOD = 1; damped_tx_all(e)
    else: damped_tx_all(e)

6. Sync & custody (T1/T2)

On any meeting: ANNOUNCE, then INV (concatenated IDs, newest first, filtered by peer's topics + carriable unicast + per-neighbor watermark), peer replies WANT, send those. INV/WANT: hops=0, unsigned, consumed, never stored/relayed. Custody: push stored unicast to any peer that is the destination or announces a fresher path. A file or sheet of paper is concatenated envelopes; import = receive. Every boat, cyclist, or HF skywave contact merges two regions. That is the WAN.

Files are content-addressed: chunks [0x07][file_id:16][index:4][bytes], indexed by a signed manifest [0x01]…[chunk_id:16 × count] whose own ID is the shareable magnet. A manifest that outgrows one envelope nests — interior nodes [0x08][depth:1]… name manifests a level down, so the root stays one frame and one signature at any file size, and only the root is signed (an ID is the hash of its bytes, so the tree authenticates itself). Sealed to one recipient: [0x09][depth:1][hdr_len:2][hdr]…, hdr = the file key + real name sealed to their prekey, each chunk then encrypted under that key with the chunk index as nonce.

7. Crypto & forward secrecy

8. Receipts (ACKREQ)

Recipient floods a signed DATA to src, payload = 0x06 + orig ID. ACKs also teach reverse paths.

9. Anonymity — mix mode

An onion is nested sealed envelopes. Sender picks 2–3 relays that follow topic mix (learned from ANNOUNCEs) and wraps inside-out: each layer = an envelope addressed to one mix, payload sealed to it = 'O' + the next full envelope. A mix decrypts, waits a random Poisson 1–30 s, batches ≥ 3, re-injects the inner envelope as its own traffic.

Honest limit: beats local observers and any subset of mixes; a global passive observer is only beaten while decoy traffic flows.

10. Self-defense (local policy)

Quotas per src and per topic; prefer keys you've met or your operator vouches for. Unsigned mail rides last. Stamp — the only cross-node priority: n leading zero bits of ID = class n, mined via a payload nonce; priority is proof of work, unforgeable on every medium. Convention: topic sos outranks policy. Confirm peers out of band by address emoji-hash.

11. Defaults

hops 16 · expiry 7 d · HELLO 5→80 min Trickle · ANNOUNCE flood ≤ 1/h · path fresh 3 h · seen-set ≥ 30 d received · prekey mint 24 h, offline window 7 d · relay airtime ≤ 10% · payload UTF-8. T0 ≈ 60 lines; full T2 ≈ 400 with libsodium.

Known limits, on purpose: no stream semantics; one envelope fountain-fragments to ≈50 KB and larger objects ride the file layer (§6), bounded by storage and by what each link agrees to carry, not by the format; no permanence (expiry is a feature); mix-mode anonymity needs flowing decoys to beat a global observer; ratchet state is per-device — give each device its own key.


Page 2 — Bindings: SPORE on everything

Every medium on Earth has one of five shapes. Bind by shape; the router never changes. This page is normative for the shapes; the per-medium parameter tables (≈70 media: frequencies, port numbers, UUIDs, MTUs, firmware caveats) are the manual, Bridges.

  1. Message pipe → one envelope/fragment per message.
  2. Byte stream → KISS: frames delimited 0xC0, escape 0xC00xDB 0xDC, 0xDB0xDB 0xDD, command byte 0x00.
  3. Text channel → armor: ~S1. + Base32(envelope) + . + Base32(SHA-256[0:4]) + ~, whitespace ignored.
  4. Shared bus → KISS + CSMA: listen-before-talk, backoff 1–5× airtime; no native CRC → append SHA-256(envelope)[0:4], verify or drop.
  5. Shared store → write envelopes as entries named by hex ID; reading = receiving; the store is a persistent INV.

Underlays with their own routing = ONE interface. Meshtastic, Reticulum, Yggdrasil, cjdns, BATMAN/OLSR, Tor/I2P, WireGuard, plain IP — each already moves bytes across many physical hops. Hand it one frame and decrement hops once for the whole crossing; its internal hops are invisible and free. Point-to-point backbone links may restore the hop so long hauls don't burn the budget. SPORE hops therefore count gateways between networks, not hops inside them — exactly IP over Ethernet.

Numbers worth memorising. Port 7373 (UDP/TCP/WS), the same value as EtherType 0x7373 and multicast 239.73.73.73 / ff02::7373. Meshtastic portnum 256. BLE rides the Nordic UART Service (6e400001-…, RX …0002, TX …0003) rather than a SPORE-specific UUID — it is what phones, hobby boards and RNodes already expose. Everything else: look it up.

Two address spaces. Who = the SPORE address or topic — end-to-end, cryptographic, identical on every medium. How = the underlay's own naming (a node number, a destination hash, an IP:port, or nothing at all) — local to one link. A bridge owns exactly one interface and translates between them; the router never learns underlay addresses, the way an OS's ARP table maps IP→MAC. Bindings are learned by snooping signed frames — a signed envelope proves its own sender, so no handshake is needed — and a stale binding costs nothing, because flood-fallback (§5.6) routes around it.

Zero-rendezvous peering (browsers & phones). A WebRTC session reduces to ufrag(4) + pwd(22) + DTLS fingerprint(32) + mDNS host candidate(16) ≈ 90 B; both sides rebuild full SDP from a hardcoded template. Beep that descriptor over ultrasound or show it as a QR, answer, and the browser's own mDNS completes a direct DataChannel — no server, no typed IP, ≈15 s. Native nodes run ice-lite with static ufrag/pwd/fingerprint, so their descriptor is a constant you can print on the box — specified, not built: WebRTC here is browser-only (web/transports/webrtc.mjs), so the native half of this is a Roadmap item. App distribution: a native node's HTTP bridge exposes the bag API — POST /spore/push, GET /spore/inv, POST /spore/want, MIME application/x-spore — on port 7373, to localhost and to the LAN at its IP. Serving the PWA itself from / is the intended end state (the app store is every node) but is not implemented: bridge::bag routes the three bag paths and 404s everything else.