Expand description
Concord v2 rekeys (CORD-06) — the per-recipient key-delivery atom + the 3303 event that carries a rotation.
A rotation mints a fresh-random key for the next epoch and delivers it only to the members who STAY, one per-recipient blob each. Two structural shifts from v1, both security-load-bearing:
D1 — the locator is PUBLIC and authenticates nothing. v1 located a blob
by HKDF(pairwise_ECDH_secret, …) — a value only the sender↔recipient pair
could compute, so in v1 a matching locator proved the blob was minted for
that pair. v2 locates by HKDF(rotator_xonly || recipient_xonly, …) — public
inputs (full NIP-46 bunker parity: a bunker computes it without a raw key).
A public locator proves NOTHING; it is a lookup index only. Authenticity now
rests entirely on (a) the crate’s rotator seal + roster authority —
verified by the caller before any blob is trusted — and (b) the blob’s
bound plaintext (scope+epoch checked after decrypt). So [open_blob]
does NOT gate on the locator (that was v1’s open_rekey_blob assumption —
do not port it): the decrypt itself (only the addressed recipient’s key
opens a blob wrapped to them) plus the bound-plaintext check are the gate.
D5 — the blob wrap carries base64. NIP-44/NIP-46 encrypt surfaces are
string-typed, and the 72 raw bytes aren’t valid UTF-8, so the wrapped
plaintext is base64(scope_id ‖ epoch_be ‖ new_key) — a string a bunker can
nip44_encrypt/nip44_decrypt to the recipient’s identity key with no raw
secret. The wrapped field is then the standard NIP-44 payload string, so a
local-keys wrap and a bunker wrap produce identical wire output.
The 3303 event itself is a v2 stream event (kind-1059 wrap, ENCRYPTED seal
signed by the rotator’s real identity) at the rekey address — reusing
super::stream. Its seal is what tells the recipient WHO rotated, which is
both the ECDH counterparty and the authority actor.
What lives here: the blob atom, the 3303 build/parse, chunk-set assembly, and the continuity/fork comparators — all PURE. The stateful orchestration (recipient-set computation, the base+channels lockstep read-cut, DB epoch archival, and the D2 BAN-vs-MANAGE_CHANNELS authority gate, which is an apply-path concern keyed on prior-vs-current-root addressing) sits in the service layer.
Structs§
- Rekey
Blob - One located, wrapped rekey blob — the unit a 3303 event carries N of.
- Rekey
Chunk - A parsed, seal-verified 3303 chunk. The
rotatoris the seal’s real signer (the ECDH counterparty AND the authority actor the caller gates on). - Rotation
- A collected rotation: all chunks sharing one correlation key, and whether the
set is complete (all
nchunks present).
Enums§
- Continuity
- The verdict of the prevcommit continuity check (CORD-06 §2).
- Rekey
Error - Errors from the rekey layer.
- Rekey
Scope - What a rekey rotates (CORD-06 §1). The 32-byte scope id is stamped into every blob’s plaintext so a blob can’t be spliced onto another coordinate.
Constants§
- MAX_
REKEY_ BLOBS_ PER_ EVENT - Max recipients (blobs) Vector puts in ONE 3303 event when SENDING. Lower than
the spec’s stated 120 because a v2 rekey rides the CORD-01 double-wrap (blob
array → encrypted seal → wrap, two NIP-44 base64 expansions): a 120-blob event
measures ~77 KB, over strfry’s 64 KB
maxEventSize, while 80 blobs measure ~55 KB (a full one is size-guarded by test). The spec’s 120 assumes a lighter envelope — a CORD-06 erratum (see the divergence ledger). A larger recipient set splits across chunk events. - MAX_
REKEY_ BLOBS_ RECEIVED - Max blobs Vector will ACCEPT in one received 3303 chunk (a DoS bound checked
after decrypt). Kept at the spec’s stated 120 — higher than the send cap — so
a chunk minted by another client at the spec limit (and delivered by a relay
with a larger
maxEventSize) still parses. An array over this is rejected.
Functions§
- am_
i_ removed - Have I been removed by this rotation? Only answerable on a COMPLETE rotation
(all
nchunks held): I’m removed iff none of the union’s blobs carries my locator. On an incomplete rotation the answer isNone— keep recovering. - base_
rekey_ group - The rekey group key for a BASE rotation — always under the PRIOR root (the one handle every retained member still holds through the rotation).
- blob_
locator - The public per-recipient locator (D1). Both parties compute it from public keys alone; it addresses the blob and nothing more.
- bound_
plaintext_ b64 - The base64 string a blob’s NIP-44 layer actually encrypts (D5). Exposed so
the service-layer bunker path can
signer.nip44_encrypt(recipient, this). - build_
blob_ local - Build one blob with LOCAL keys (the bunker path drives the same wire via the
_b64helpers + a NIP-46nip44_encrypt). The wrap is the pairwise conversation keyConversationKey::derive(rotator_sk, recipient_pk), so only the recipient’s identity key opens it. - build_
rekey_ chunks_ local - Split a full recipient blob set into 3303 chunk events (≤120 blobs each),
all sharing the rotation’s
(scope, new_epoch, prev_commit)so a receiver correlates them. Local-keys convenience. - build_
rekey_ rumor - Build the unsigned 3303 rumor (rotator is the pubkey; the seal will carry the signature). Enforces the monotonic-epoch and chunk-range invariants at mint.
- channel_
rekey_ group - The rekey group key for a CHANNEL rekey addressed under
addressing_root. The caller chooses the root: a STANDALONE channel rekey rides the CURRENT root (MANAGE_CHANNELS); a channel rekey forced by a removal rides the PRIOR root alongside the base rekey (D2 — inherits the removal’sBANauthority, and the prior-root address is exactly what distinguishes the two classes on the wire so a base-fork loser can still open it). - check_
continuity - Check a rotation’s
prev_commitagainst the(epoch, key)I currently hold for its scope. A match proves the rotation extends the very key I hold; a higherprev_epochmeans I’m behind; anything else is a fork/garbage. - collect_
rotations - Group a batch of parsed chunks into rotations by correlation key. Chunks whose
ndisagrees with their siblings, or that repeat an index, are the caller’s concern to police; here the first-seennper correlation wins and duplicate indices are ignored (idempotent re-delivery). - find_
my_ blob - Find my blob in a chunk’s array by my public locator (the lookup step, D1).
Nonemeans this chunk doesn’t carry my key — never a removal on its own (only “removed” once ALL chunks are held and none has it). - lowest_
key_ winner - Deterministic same-epoch fork winner (CORD-06 §3): among candidate rotations
at one continuity point, the one whose decrypted
new_keyis lexicographically lowest wins. Every retained member decrypts its own blob from each fork and computes the identical winner. Returns the index intocandidatesof the winner, orNoneif the caller decrypted no candidate. - open_
blob_ local - Open a blob addressed to me with LOCAL keys. Per D1 this does NOT check the locator: the decrypt (only my identity key opens a blob wrapped to me by the rotator) plus the bound scope/epoch ARE the authenticity boundary. A blob relocated to a foreign locator still won’t decrypt for a non-recipient, and a spliced one fails the bound check.
- parse_
bound_ plaintext - Parse + verify a decrypted bound plaintext (the base64 already stripped),
checking scope+epoch strict-equal the coordinate it was opened under before
yielding
new_key. Exposed for the bunker open path. - parse_
rekey_ chunk - Parse a 3303 chunk from a seal-verified stream open. Rejects a non-3303 rumor, a plaintext seal (the rekey plane is encrypted-only), malformed or duplicate machinery tags, a bad chunk range, and an over-cap blob array.
- seal_
rekey_ chunk - Seal + wrap a 3303 rumor into its stream event at the rekey address. The seal is ENCRYPTED (20013 — the rekey plane MUST NOT be plaintext-sealed) and signed by the rotator; the wrap by the rekey group key.
Type Aliases§
- Rotation
Key - The key that groups chunks of ONE rotation:
(rotator, scope_id, new_epoch, prev_commit). Two rotators racing the same epoch, or one rotator over two channels, never alias.