spec: close the remaining open design decisions - #79
Conversation
Resolves the design decisions that the review of the batched spec left
open. The bulk of the key-custody, receive-in-recursion and transport
work already landed; this closes what remained.
Soundness — bind the nullifier key to the account identity:
- add nk_commit = Hc("NkCommit", nk) as a committed AccountState field
(§1.5, §1.7.4, part of the ash preimage) and derive the address from
it: address = H(Pk0 ‖ nk_commit) (§1.4). Two accounts under one
address are no longer possible, so a received coin has exactly one
valid nullifier and account<->address is genuinely one-to-one.
- §2.1: clause 1 (InitialProof) fixes nk_commit and the address at
genesis; clause 4 checks the witnessed nk against prev nk_commit
before deriving any nullifier; clause 7 carries nk_commit forward
unchanged (like owner). This closes both the same-prev-state fork and
the genesis-equivocation double-spend the accumulator alone did not
catch.
- carry nk_commit through Invoice/addressing (§4.3), the ownership
proof (§5.1), the mint owner-binding (§2.3.1/§6.5), recovery (§4.5,
reproducible from the seed), the glossary and test vectors V.2/V.3/V.4
(affected pinned values marked <REGEN>).
- §2.6: the genesis owner-binding is a fourth in-circuit SHA-256 site.
- §6.5/§1.4: asset_id binds Pk0 alone, so one Pk0 may issue under
several accounts (one-to-many) — benign for v1, noted for a future
auditable-supply schema.
Bounded balances (buildability):
- §2.5 pins MAX_ACCOUNT_ASSETS = 32 and the balances slots follow the
inactive-sentinel padding of the input/output slots; §1.7.4 notes the
in-circuit padded absorption equals the out-of-circuit variable-length
ash bit-for-bit (inactive slots contribute nothing).
Transport and access fixes:
- §7.5/§3.6: /v1/chain/inscriptions returns blob_id; fetch a BatchBundle
by blob_id (resolved from the inscription), then bind by recomputing
bundle_locator — consistent with the plaintext-blob model of §4.2.1.
- §5.1: a normative pull-session — POST /v1/pull issues a short-lived
bearer token bound to chan_bind/subject/scope with its own expiry;
GET /v1/proof and the kernel GetCoinProof reference it.
- requirements 9(b) reconciled with the §5.7 balance attestation (the
single anchor is a documented v1 limit); §5.7's leak-free-anchor path
raised to a normative future-version clause.
- §2.5 recursion-tree wording aligned with the member_root shape.
…odel The glossary BatchBundle entry, the §1.1 domain-tag list, and the V-section byte layout still labelled bundle_locator a 'content address'; a BatchBundle is fetched by blob_id and bundle_locator is the on-chain binding value (§3.6).
|
Ready for review. This went through 5 review passes (build+review loop, two lenses each round: conformance/consistency + logic/soundness) until both lenses came back clean. The soundness lens earned its keep in pass 1: the initial Out of scope, noted for a possible follow-up: three pre-existing (base |
Why
The review of the batched spec identified six design decisions that were still open after the key-custody / receive-in-recursion / transport work landed (#74). This PR closes them. The biggest is a real soundness gap; the rest range from a buildability bound to small consistency fixes.
What
Soundness — bind the nullifier key to the account identity (double-spend)
The nullifier key
nkwas an unconstrained circuit witness and was not committed anywhere inAccountState. Because the address wasH(Pk₀)— independent ofnk— the same address could back two accounts with differentnk, letting the holder spend one received coin under two distinct nullifiers that the accumulator does not collide. This closes it:nk_commit = Hc("NkCommit", nk)is a committedAccountStatefield (§1.5, §1.7.4, part of theashpreimage), and the address now derives from it:address = H(Pk₀ ‖ nk_commit)(§1.4). Differentnk⇒ different address, so a received coin has exactly one valid nullifier and account ↔ address is genuinely one-to-one.nk_commitand the address at genesis; clause 4 checks the witnessednkagainstprev.nk_commitbefore deriving any nullifier; clause 7 carriesnk_commitforward unchanged (likeowner). This closes both the same-prev-state fork and the genesis-equivocation variant.nk_commitis carried through Invoice/addressing (§4.3), the ownership proof (§5.1), the mint owner-binding (§2.3.1/§6.5), recovery (§4.5 — reproducible from the seed), the glossary, and test vectors V.2/V.3/V.4 (affected pinned values marked<REGEN>).asset_idbindsPk₀alone, so onePk₀may issue under several accounts (one-to-many) — benign for v1 (minting still needssk₀), noted for a future auditable-supply schema.Bounded balances (buildability)
MAX_ACCOUNT_ASSETS = 32; the balances slots follow the same inactive-sentinel padding as the input/output slots. §1.7.4 notes the in-circuit padded absorption equals the out-of-circuit variable-lengthashbit-for-bit (inactive slots contribute nothing), so the fixed-shape circuit can computeashdeterministically.Transport & access
/v1/chain/inscriptionsreturnsblob_id; a scanner fetches aBatchBundlebyblob_id(resolved from the inscription) and binds it by recomputingbundle_locator— consistent with the plaintext-blob model of §4.2.1/§7.4.POST /v1/pullissues a short-lived bearer token bound tochan_bind/subject/resolved scope with its own expiry;GET /v1/proofand the kernelGetCoinProofreference it (resolves the previously dangling term).member_rootshape.Review
Built through a build+review loop: three review passes across two lenses (conformance/consistency + logic/soundness). Pass 1 caught that the initial
nk_commitfield boundnkonly to a lineage, not the coin-owning identity — the fix (address = H(Pk₀ ‖ nk_commit)) closes the genesis-equivocation variant and was verified in passes 2–3. The soundness chain (fork + genesis double-spend fully closed, account ↔ address one-to-one, no guarantee weakened,nkstays a secret witness) was confirmed clean.