A single-use token is worthless if you can use it twice. But "remember every token ever spent" is both a metadata store and an unbounded database: two things this system refuses to build. The double-spend ledger is the smallest possible memory that closes the gap. Its whole subtlety is in one number: how long to remember.
This is the state-keeping counterpart to The Admission Token. It is short, because the mechanism is short; the interesting part is why the timing works.
An admission token is a (nonce, token) pair. The nonce
(cryptography's word for a number used once) is a random serial number the client picked. The token is
the value the
mint (the token-issuing service) computed for that serial number without ever seeing it. The
relay accepts each pair exactly once. Nothing about the cryptography stops a client from attaching the same
pair to a thousand envelopes: the token verifies fine each time. Only memory stops a replay,
the trick of re-presenting an old but still-valid token:
the relay must record that a nonce has been spent and reject its reappearance. That memory is
the double-spend ledger.
Two constraints make it delicate:
The entire design is threading between those two: forget, but never a second before it is safe.
The check is as small as it can be. The ledger is a key-value store (Redis, an in-memory database). Each spend is a single atomic operation: one indivisible step that either happens completely or not at all. The entry is written with an expiry (a TTL, "time to live") after which the store deletes the entry on its own:
SETNX tokens:redeemed:<key_id>:<nonce> with a TTL
SETNX ("set if not exists") is the whole trick. It atomically does two things: if the
key is absent, it writes it and reports "fresh"; if the key is already there, it writes
nothing and reports "already spent." Because it is one atomic decision, there is no
read-then-write race: the classic bug where two requests each check, each see "absent," and
both get accepted. Two simultaneous spends of the same nonce cannot both win. (The expiry is a second command:
SETNX cannot carry a TTL in one round trip here, so the relay pays two.)
The relay accepts the token if and only if SETNX reports "fresh." There is nothing else to
it: no per-user record, no recipient, no content. The ledger key is (key_id, nonce) and
nothing more, so the one piece of state the
anonymous system keeps is itself anonymous: a pile of "this serial number is used"
markers with no one's name on them.
That last claim leans on something outside this chapter, and it is worth being explicit about
the dependency. key_id, the cleartext (unencrypted) label naming which issuing key minted the token, is
anonymous only because the live key set is forced to be short, public, and identical for every
client.
If each user had their own issuing key, this same table would be a per-user spend log: the ledger would quietly become a deanonymising artefact, a piece of stored data that ties actions back to people, which is exactly what the rest of the design works to avoid. The checks that prevent it live in The Admission Token §5, and they are load-bearing here as much as at the mint.
Rollback on later rejection. Admission runs more checks after the spend (notably the check that the sender's certificate has not been revoked). If one of those later checks rejects the envelope, the relay rolls back the spend: it deletes the just-written marker. That way a message dropped for an unrelated reason does not burn an otherwise-good token. You only lose the token if the token itself was the problem.
Now the crux. What TTL does the ledger entry get? Too short and you reopen the double-spend hole; too long and you keep useless state (and, worse, a longer-lived database). The answer falls out of three durations that have to line up:
s (the mint's secret, of which the
relay holds a copy so it can recompute a token and verify it)
rotates daily. A token minted under yesterday's key is checked against yesterday's key.Put the three together and the guarantee is airtight: at every instant, a token is either (a) still redeemable, in which case its nonce is still in the ledger, or (b) past its 30-hour horizon, in which case it is refused by the key-redemption window before the ledger is even consulted.
There is no window in which a token is simultaneously acceptable and forgotten. The double-spend gap is closed not by remembering forever, but by remembering for precisely the interval a token can matter, and refusing anything older on a different check.
The image is a nightclub hand-stamp tuned to fade exactly at closing time. While the club is open your stamp is visible and you can't get a second one; the moment the stamp could no longer be honored anyway, it fades. The bouncer never has to remember last week's stamps, because last week's stamps wouldn't be honored regardless.
It is worth appreciating how little state this is. The anonymous-admission system
(sender hidden, unlinkable,
no mailbox) keeps exactly one memory for admission: a set
of (key_id, nonce) markers that each evaporate in a bit over a day and name no one. There
is no "spends per user" table (there is no user), no "who sent to whom" (the ledger doesn't
see recipients), nothing to mine.
The relay holds two other bounded things, neither of which names a user either: the short window of opaque ciphertext that makes oblivious pull possible, and a first-claimer binding for each published discovery address (Who Gets to Listen). Stopping replay forces exactly one concession of its own: a self-erasing, anonymous set of serial numbers, about as close to "stateless" as a system that enforces single-use can get.