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
SRVrecords — 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, thesithbit discoverresolver, 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
MailDomainaccount’s recorded authority on-chain. - Clients discover a domain’s nodes via a DHT lookup + on-chain
verification, with no public DNS
SRVquery. - 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:
| Primitive | In 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 on | ipfs_swarm runs libp2p Kademlia with put_record/get_record and a validation hook. |
| Fleet membership for one operator | The IPFS clustering wave: shared-bucket membership roster + rendezvous-hash keyspace partition. |
| Shared state across nodes | The 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
58888arc 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):
| Key | Default | Meaning |
|---|---|---|
service_record_ttl_secs | 900 (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_secs | 300 (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
MailboxNotifyseam —MailRepo::await_change, with a poll-backed default overchange_seq(theDEFAULT_WATCH_POLLinterval, 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 (PostgresLISTEN/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_changeseam are shipped, but native cross-process IMAP-IDLE push per backend is still deferred — thechange_seqpoller 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
SRVis 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.1extension 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 discoveris 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.