Phase 1: Nodes Find Each Other
2026-02-14
Tesseras is no longer a local-only tool. Phase 1 delivers the networking layer: nodes discover each other through a Kademlia DHT, communicate over QUIC, and publish tessera pointers that any peer on the network can find. A tessera created on node A is now findable from node C.
What was built
tesseras-core (updated) — New network domain types: TesseraPointer
(lightweight reference to a tessera's holders and fragment locations),
NodeIdentity (node ID + public key + proof-of-work nonce), NodeInfo
(identity + address + capabilities), and Capabilities (bitflags for what a
node supports: DHT, storage, relay, replication).
tesseras-net — The transport layer, built on QUIC via quinn. The Transport
trait defines the port: send, recv, disconnect, local_addr. Two adapters
implement it:
QuinnTransport— real QUIC with self-signed TLS, ALPN negotiation (tesseras/1), connection pooling via DashMap, and a background accept loop that handles incoming streams.MemTransport+SimNetwork— in-memory channels for deterministic testing without network I/O. Every integration test in the DHT crate runs against this.
The wire protocol uses length-prefixed MessagePack: a 4-byte big-endian length
header followed by an rmp-serde payload. WireMessage carries a version byte,
request ID, and a body that can be a request, response, or protocol-level error.
Maximum message size is 64 KiB.
tesseras-dht — A complete Kademlia implementation:
- Routing table: 160 k-buckets with k=20. Least-recently-seen eviction, move-to-back on update, ping-check before replacing a full bucket's oldest entry.
- XOR distance: 160-bit XOR metric with bucket indexing by highest differing bit.
- Proof-of-work: nodes grind a nonce until
BLAKE3(pubkey || nonce)[..20]has 8 leading zero bits (~256 hash attempts on average). Cheap enough for any device, expensive enough to make Sybil attacks impractical at scale. - Protocol messages: Ping/Pong, FindNode/FindNodeResponse, FindValue/FindValueResult, Store — all serialized with MessagePack via serde.
- Pointer store: bounded in-memory store with configurable TTL (24 hours default) and max entries (10,000 default). When full, evicts pointers furthest from the local node ID, following Kademlia's distance-based responsibility model.
- DhtEngine: the main orchestrator. Handles incoming RPCs, runs iterative
lookups (alpha=3 parallelism), bootstrap, publish, and find. The
run()method drives atokio::select!loop with maintenance timers: routing table refresh every 60 seconds, pointer expiry every 5 minutes.
tesd — A full-node binary. Parses CLI args (bind address, bootstrap peers, data directory), generates a PoW-valid node identity, binds a QUIC endpoint, bootstraps into the network, and runs the DHT engine. Graceful shutdown on Ctrl+C via tokio signal handling.
Infrastructure — OpenTofu configuration for two Hetzner Cloud bootstrap
nodes (cx22 instances in Falkenstein, Germany and Helsinki, Finland). Cloud-init
provisioning script creates a dedicated tesseras user, writes a config file,
and sets up a systemd service. Firewall rules open UDP 4433 (QUIC) and restrict
metrics to internal access.
Testing — 139 tests across the workspace:
- 47 unit tests in tesseras-dht (routing table, distance, PoW, pointer store, message serialization, engine RPCs)
- 5 multi-node integration tests (3-node bootstrap, 10-node lookup convergence, publish-and-find, node departure detection, PoW rejection)
- 14 tests in tesseras-net (codec roundtrips, transport send/recv, backpressure, disconnect)
- Docker Compose smoke tests with 3 containerized nodes communicating over real QUIC
- Zero clippy warnings, clean formatting
Architecture decisions
- Transport as a port: the
Transporttrait is the only interface between the DHT engine and the network. Swapping QUIC for any other protocol means implementing four methods. All DHT tests use the in-memory adapter, making them fast and deterministic. - One stream per RPC: each DHT request-response pair uses a fresh bidirectional QUIC stream. No multiplexing complexity, no head-of-line blocking between independent operations. QUIC handles the multiplexing at the connection level.
- MessagePack over Protobuf: compact binary encoding without code generation or schema files. Serde integration means adding a field to a message is a one-line change. Trade-off: no built-in schema evolution guarantees, but at this stage velocity matters more.
- PoW instead of stake or reputation: a node identity costs ~256 BLAKE3 hashes. This runs in under a second on any hardware, including a Raspberry Pi, but generating thousands of identities for a Sybil attack becomes expensive. No tokens, no blockchain, no external dependencies.
- Iterative lookup with routing table updates: discovered nodes are added to the routing table as they're encountered during iterative lookups, following standard Kademlia behavior. This ensures the routing table improves organically as nodes interact.
What comes next
- Phase 2: Replication — Reed-Solomon erasure coding over the network, fragment distribution, automatic repair loops, bilateral reciprocity ledger (no blockchain, no tokens)
- Phase 3: API and Apps — Flutter mobile/desktop app via flutter_rust_bridge, GraphQL API (async-graphql), WASM browser node
- Phase 4: Resilience and Scale — ML-DSA post-quantum signatures, advanced NAT traversal, Shamir's Secret Sharing for heirs, packaging for Alpine/Arch/Debian/FreeBSD/OpenBSD, CI on SourceHut
- Phase 5: Exploration and Culture — public tessera browser, institutional curation, genealogy integration, physical media export
Nodes can find each other. Next, they learn to keep each other's memories alive.