spec: hide the next_pubkey rotation edge behind a per-transition commitment - #86
Conversation
…itment PR #85 bound next_pubkey into the signed SpendRecord message to authorise the key rotation on every transition (custody). But the raw next_pubkey = Pk(i+1) then sat in the clear in the public, plaintext BatchBundle, next to each record's public_key = Pk(i). Because next_pubkey(txn i) == public_key(txn i+1), any observer fetching the public bundles could chain an account's consecutive SpendRecords by the rotation edge -- a public, cross-transition account linkage that contradicts the transaction-graph unlinkability the spec states for a BatchBundle scanner (2.3.2, 3.5, 5.5) and Requirement 2. Make the third message segment a hiding commitment next_pubkey_commit = H("zkCoins/v1/NextPubkeyCommit" || next_pubkey || npk_rand) instead of the raw key. The wallet draws a fresh secret npk_rand per transition (SHA-256, wallet-native, no Poseidon) and signs the commitment; the per-account circuit (clause 2) additionally checks the opening against the same w.next_pubkey clause 7 folds into the account state. Custody is preserved exactly as tightly -- a party without sk(i) can neither sign a different commitment nor open the given one to another key -- while the published value is hiding, so the rotation edge can no longer chain records. npk_rand is a circuit witness only, carried wallet->node in the /sign body, never in a SpendRecord, BatchBundle, CoinProof, or to a publisher. Message stays 96 bytes; all record sizes unchanged.
|
One review pass (soundness + consistency lenses) with zero findings. The soundness lens confirmed custody is preserved exactly as tightly as the raw-key signature — a party without skᵢ can neither sign a different commitment nor open the given one to another key (SHA-256 collision/second-preimage resistance), and the clause-2 opening ties the committed value to the same w.next_pubkey clause 7 folds into the account state — while the privacy regression is genuinely closed: the raw next_pubkey appears in no public object, npk_rand is a 256-bit secret witness never placed in any SpendRecord/BatchBundle/CoinProof/publisher body, so a bundle scanner can no longer chain records by the rotation edge. The consistency lens independently recomputed the pinned npk_rand@0 SHA-256, confirmed message stays 96 bytes with all record sizes unchanged, and verified the previously contradictory §2.3.2/§3.5/§5.5 unlinkability statements are now true. |
Problem
PR #85 bound
next_pubkeyinto the signedSpendRecordmessageso the account's single transition signature authorises the key rotation on every transition (custody — closes the operational-bundle-node account takeover via an unbatched receive).But it placed the raw
next_pubkey = Pkᵢ₊₁in the clear in the public, plaintextBatchBundle, next to each record'spublic_key = Pkᵢ. Sincenext_pubkey(txn i) == public_key(txn i+1), any observer fetching the public bundles can chain an account's consecutiveSpendRecords by the rotation edge — a public, cross-transition account linkage of the transaction graph.This contradicts the unlinkability the spec itself states against a
BatchBundlescanner:SpendRecordto the account's prior records"It is a regression: before #85,
message = inr ‖ ocr(64 B) andnext_pubkeywas witness-only, so the rotation edge was never published.Fix
Make the third
messagesegment a hiding commitment rather than the raw key:next_pubkey_commit = H("zkCoins/v1/NextPubkeyCommit" ‖ next_pubkey ‖ npk_rand)(SHA-256, wallet-native, no Poseidon;npk_rand= fresh secret 32-byte blind the wallet draws per transition).message = inr ‖ ocr ‖ next_pubkey_commit— still 96 bytes; allSpendRecordsizes unchanged (193 + 32·|nf|, 225 single-input, 193 mint).next_pubkey_commit == H(tag ‖ w.next_pubkey ‖ w.npk_rand)against the samew.next_pubkeyclause 7 folds into the account state. Custody is preserved exactly as tightly (SHA-256 collision resistance: a party withoutskᵢcan neither sign a different commitment nor open the given one to another key).public_key = Pkᵢ₊₁cannot recompute the commitment without the secretnpk_rand, so the rotation edge no longer chains records — the §2.3.2/§3.5/§5.5 claims become true again.npk_randis a circuit witness only, carried wallet→node in the/signbody; never in aSpendRecord,BatchBundle,CoinProof, or to a publisher.Single file (
docs/specification.md). Test-vector blindnpk_rand@0 = H("zkCoins/v1/test-vector/npk_rand")pinned (verified SHA-256);next_pubkey_commit@0left<REGEN>.