Architecture
Address Derivation¶
Endpoints are derived, not announced. A client computes today's address for a cell from a secret and the clock; the cell computes the same address independently. Nobody publishes anything. There is no DNS record to seize, no static address in the binary to scan for.
The formula¶
| Input | Role |
|---|---|
ipv6_prefix |
The fixed network portion — a /64 (or wider) the cell controls. |
cell_secret |
The client's credential for this cell. The only secret here. |
domain_tag |
Domain separation, so a cell secret can never collide with output from another use. |
bucket |
The time window — which hour it is. From the clock. |
slot |
Which of the cell's concurrent addresses — enumeration, 0..slot_count. |
truncate takes the leading host-width bits of the HMAC output and uses them
directly as the host portion of the address. Host width = 128 − prefix_length
(a /64 gives 64 bits). HMAC-SHA256 produces 256 bits, so even a /48 uses less than
a third of the output.
The grid: two independent dimensions¶
bucket and slot are unrelated. One is time; one is fan-out. Together they
form a grid, and at any instant a cell binds the whole live sub-grid:
slot 0 slot 1 slot 2 ... slot 29
bucket 12344 [addr] [addr] [addr] ... [addr] ← previous hour (skew)
bucket 12345 [addr] [addr] [addr] ... [addr] ← current hour
bucket 12346 [addr] [addr] [addr] ... [addr] ← next hour (skew)
With skew 1 and 30 slots, that is 90 addresses bound simultaneously. Rows roll over every hour; columns stay put. The time-bucket module produces the row numbers; this derivation fills in each cell of the grid.
- Slots give resilience: one address being blocked or blackholed does not take the cell down — 29 others are live.
- Buckets give rotation and clock tolerance: the skew rows are what let a client with a slightly wrong clock still land on a bound address.
They are independent dials at different layers. Slot count is a per-cell resilience decision; it has nothing to do with how many cells a client holds.
Two operations people confuse¶
The credential is the cell secret, never an address
- Unbinding an address — a cell stops listening on one endpoint. The client can still derive it; it just finds nothing there. An availability action, not revocation.
- Rotating the cell secret — invalidates every address that secret ever produced, past and future, instantly. This is real revocation, and it is how a client goes from N cells to N−1 in under a minute.
Because every address is HMAC(cell_secret, …), rotating the secret produces an
entirely disjoint address sequence. A client holding the old secret cannot derive
a single valid address anymore. You are not deleting addresses; you are
invalidating the thing that generates them.
An address is a rendezvous point, not a credential¶
This is the most important framing on the page. In TOTP, the derived code is a credential — holding it proves you know the secret. Here it is not. Anyone who learns today's address may connect to it, and will fail the Noise handshake, because authentication chains to the root key and has nothing to do with how the endpoint was discovered.
Consequences:
- No rate limiting on derivation — there is nothing to brute-force that helps.
- No single-use enforcement — addresses are reused within a bucket by design.
- Learning an address is not a compromise — it is a scanning shortcut, and scanning a /64 is infeasible anyway (2⁶⁴ host addresses).
Secrecy of the address buys scan-resistance and unlinkability, not authentication. Keep those two roles apart permanently — conflating "hard to find" with "authenticated" is exactly the class of composition error that kills protocols.
Why HMAC truncation is safe¶
HMAC is a pseudorandom function: its output is computationally indistinguishable
from random, so any subset of its bits is too. Taking the leading 64 bits
leaks nothing about cell_secret — you would have to invert HMAC to work
backward, and truncation discards information rather than exposing it.
Collisions are a non-issue: by the birthday bound you would need ~2³² addresses in a single /64 before a 50% collision chance. Dozens of slots is nothing. (A property test still asserts distinctness — and skips the handful of reserved interface IDs if a derivation ever lands on one.)
Read next: the time buckets that produce the bucket input —
the module currently under construction.