Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decentralized service discovery for mail-access servers

Status: implemented. SithBit clients can discover a domain’s POP/ IMAP servers over SithBit’s own DHT — no DNS SRV records — and each node proves it is authorized to serve the domain cryptographically, chaining to the on-chain domain identity. This page describes the shipped system: the node-delegation certificates, the signed DHT service records, the self-authenticating TLS handshake, the sithbit discover resolver, and the failover seam. It composes on top of the IPFS swarm and clustering work.

Read the honest limitations before you lean on it: discovery and authentication are the easy parts; cross-node maildrop/IMAP-state consistency is the hard part and presupposes a shared cloud store.

The idea

Classically a mail client finds a domain’s POP/IMAP servers through the DNS SRV/A records the operator publishes. SithBit already avoids DNS for addressing (a wallet address is the mailbox; DNS is used only for domain verification TXT records and MX). This closes that loop for the mail-access protocols: clients discover a domain’s POP/IMAP nodes over SithBit’s own DHT, and each node proves its authority to serve the domain cryptographically rather than by DNS assertion.

Concretely:

  • A node advertises itself under a DHT key derived from the on-chain domain identity ( hash("sithbit/service-record/v1" ‖ domain ‖ proto)).
  • A node vouches for its authority by proving control of a key the domain authority signed for it — chaining to the MailDomain account’s recorded authority on-chain.
  • Clients discover a domain’s nodes via a DHT lookup + on-chain verification, with no public DNS SRV query.
  • Operators scale by running more nodes; each advertises itself. A domain that runs a single SQLite node is the degenerate one-node case — the owner’s prerogative and their burden for their own correctness.

The guiding split is authority on-chain, endpoints and liveness off-chain: who may serve a domain changes rarely and belongs on the chain; which boxes are up right now changes constantly and belongs on the DHT.

What it is built from

Most of this is assembly of primitives SithBit already owned:

PrimitiveIn the repo
Root of trust for “who owns this domain”MailDomain PDA records the domain authority pubkey; SendMail gates signer == recipient domain authority.
A DHT to advertise onipfs_swarm runs libp2p Kademlia with put_record/get_record and a validation hook.
Fleet membership for one operatorThe IPFS clustering wave: shared-bucket membership roster + rendezvous-hash keyspace partition.
Shared state across nodesThe cloud stores (AWS/Azure/Turso/Cloudflare) — many processes over one store.

The genuinely new work — the node_cert crate (node-delegation certificates), the signed service-record type in ipfs_swarm, the self-authenticating TLS verifier in mail_client, the sithbit discover resolver, and the failover seam in mail_store/imap_server — is what the rest of this page describes.

Delegation — the key design decision

The domain’s on-chain authority private key never goes on a POP/IMAP box: that key can authorize/deactivate domains and sign SendMail. Instead the authority issues node-delegation certificates (the node_cert crate): a short-lived, borsh-serialized NodeDelegation { domain, proto, node_pubkey, expiry }, Ed25519-signed by the authority into a SignedDelegation { delegation, sig }. A node holds only its delegated key; both the DHT record and the TLS certificate are signed by that node key, and clients verify the delegation chains to the on-chain MailDomain.authority.

node_cert is pure crypto — borsh plus Ed25519 sign/verify, no chain RPC and no X.509 in the core type (the X.509 wrapping is a separate module). Proto is a borsh enum (Pop, Imap) whose variant order is wire ABI: only ever append.

This mirrors, philosophically, SithBit’s existing delegated X25519 encryption-key pattern (mailbox key set) — a wallet publishing a delegated key so signing-only wallets and MX servers never touch the root key. It is not the same mechanism: node-delegation certs are self-delivered (carried in the DHT record and the TLS handshake), not registered on-chain. A stolen node key is bounded in time and scope; revocation is “let the delegation expire / rotate it.”

Proving authority in two places (belt and suspenders)

The one node key the authority delegated signs both the discovery record and the TLS certificate, and each proof independently chains back to the on-chain MailDomain.authority:

1. In the discovery record

A node publishes a signed ServiceRecord { domain, proto, multiaddrs, ttl_secs, created_at, signed_delegation, node_sig } under the DHT key hash("sithbit/service-record/v1" ‖ domain ‖ proto) — a keyspace separate from the CID provider records — via Kademlia put_record. On get, a validation hook runs ServiceRecord::validate, which is entirely self-contained (no chain RPC on the DHT path):

  • the node signature verifies against the key the delegation binds (node_pubkey) — a forged or tampered body is rejected;
  • the record’s (domain, proto) matches the delegation’s (a delegation for one service can’t be replayed under another);
  • the delegation has not expired (now < delegation.expiry);
  • the advertise TTL is fresh (now < created_at + ttl_secs).

This stops anyone from planting a record for a domain they do not control, and ages out records from nodes that stopped heartbeating — without a chain lookup on the hot DHT path. The on-chain-authority check is the client’s job, below.

2. In the connection (self-authenticating TLS)

A POP/IMAP node presents a self-signed TLS certificate carrying its SignedDelegation in a custom X.509 v3 extension under OID 1.3.6.1.4.1.58888.1.1, and the certificate’s public key is the delegated node key. A client’s NodeDelegationVerifier (a rustls ServerCertVerifier) extracts the delegation, resolves the domain’s MailDomain.authority from chain, verifies the delegation against it, and enforces that the certificate key equals the delegated node_pubkey. Trust flows only from the chain: intermediates, the SNI/server name, and OCSP are deliberately ignored. This is impersonation-proof even if the DHT is poisoned — a poisoned record just points a client at a box whose TLS handshake then fails the chain check.

The OID’s 58888 arc is an unregistered placeholder private enterprise number — SithBit has not registered a PEN with IANA; the arc was picked arbitrarily. Anyone building an independent verifier must match this constant exactly. Registering a real PEN is a tracked external prerequisite (an IANA application, not in-code work) that MUST be completed and the OID updated before mainnet; until then this arc is unregistered and collision-prone.

Configuring service records ([swarm])

Service advertisement lives in the swarm config — [ipfs.swarm] for sithbitd, [swarm] for sithbit-ipfsd — alongside the existing listen/bootstrap/provide/identity settings. Two settings govern freshness; both have in-code defaults, so an empty [swarm] is still valid (a plain node that never advertises a service ignores them entirely):

KeyDefaultMeaning
service_record_ttl_secs900 (15 min)How long an advertised record stays fresh from created_at. Deliberately minutes-scale so a node that stops heartbeating ages out of discovery quickly — liveness wants minutes, not the ~22 h content-reprovide cadence.
service_heartbeat_interval_secs300 (5 min)How often the node re-stamps created_at and re-publishes its records. Must stay comfortably below the TTL so a record never lapses between heartbeats.

See the configuration reference for the shared subsection.

Discovering nodes (sithbit discover)

The client-side resolver is sithbit discover pop|imap <domain>:

sithbit discover imap sithbit.com \
  --bootstrap /ip4/203.0.113.7/tcp/4001/p2p/12D3Koo... \
  --timeout-secs 20

It spins an ephemeral, discovery-only embedded swarm node (it never pins — the repo store is inert), seeds the DHT routing table from the --bootstrap peers (repeatable; without at least one reachable peer the routing table is empty and nothing is found), looks the (domain, proto) service records up, and prints only the endpoints whose node-delegation chains to the domain’s on-chain MailDomain.authority. Verified multiaddrs go to stdout; the count of records that survived DHT validation but failed the chain check goes to stderr as a dropped-record note.

The resolver grants no trust: it only speeds discovery. A real client still verifies the TLS certificate against chain itself (the self-auth handshake above), so a resolver that returned a bad address could not fool the connection. Full in-client libp2p (Thunderbird/Outlook/wasm) remains a heavier later optimization; the resolver is the light path.

See sithbit-ipfsd: discovering a domain’s nodes for the operator-side view.

Failover consistency

Discovery is only safe if a client that fails over from node A to node B sees the same mailbox. Two seams make that hold over a shared store:

  • A cluster-wide keyed lease pop/{wallet} prevents two nodes serving one POP maildrop concurrently.
  • A cross-process MailboxNotify seam — MailRepo::await_change, with a poll-backed default over change_seq (the DEFAULT_WATCH_POLL interval, 2 s) — lets a failed-over IMAP IDLE client on one node learn of mail delivered on another, on every backend including SQLite. Native per-backend push (Postgres LISTEN/NOTIFY, DynamoDB Streams) is a deferred drop-in behind the same seam.

This is only meaningful over a shared cloud store. On SQLite it is genuinely single-node by design — the accepted owner’s-prerogative case.

Honest limitations — the auth is the easy part

These are deliberate and unchanged from the original design note; do not read the “implemented” banner as “all hard problems solved”:

  • Only meaningful over the shared cloud store. POP/IMAP are stateful and session-pinned. Cross-node failover requires a cloud store; on SQLite it is single-node. The SQLite→cloud migration tool is the on-ramp.
  • Maildrop-lease and IMAP-state consistency is the deep part. The keyed lease and the poll-backed await_change seam are shipped, but native cross-process IMAP-IDLE push per backend is still deferred — the change_seq poller is today’s cross-process path. This, not discovery/auth, is the real engineering.
  • Enumeration / DoS surface. A public DHT advertising POP/IMAP endpoints is trivially scannable (DNS SRV is at least obscure). server_common’s connection limits and DNSBL/DBL apply; high-value domains may want gated or encrypted-to-known-clients discovery records.
  • Placeholder OID/PEN. The 1.3.6.1.4.1.58888.1.1 extension arc is an unregistered placeholder, not an IANA-registered enterprise number. Registering a real PEN is a tracked external prerequisite that MUST land before mainnet.
  • Resolver, not client-native discovery. sithbit discover is a CLI resolver; discovery is not yet wired into the Thunderbird/Outlook/wasm clients themselves, nor into alias-based login. Those remain candidate follow-ups.

Verdict

A strong fit, and largely an assembly of pieces SithBit already owns — on-chain domain authority, libp2p Kademlia, the clustering roster, Ed25519 self-auth certs, and the cloud shared store — now shipped. It is philosophically on-brand: a decentralized mail protocol that already avoids DNS for addressing now discovers its own access servers without DNS SRV. Go in eyes-open that discovery is the easy part; cross-node maildrop/IMAP-state consistency over the shared store is the hard part, and that the whole scheme presupposes the cloud-store deployment.