Skip to main content

Implementation Mandate

A standing instruction to every implementor (human or agent) building zkCoins. It is deliberately short on "how to code" and exact on what done means, what the source of truth is, and how to make decisions without asking.

0. Status: green field

zkCoins is green field. Nothing in production must be preserved. There is no data to migrate, no API contract to keep stable, and no backward compatibility to honour. Every existing artefact in the node, api, sdk, and app repositories is a prototype and MAY be rewritten, replaced, or deleted in full. You are free to design the codebase from scratch. The only fixed point is the protocol defined in the Specification.

This freedom is also a responsibility: where the current code disagrees with the spec, the spec wins and the code is changed — never the other way around without a spec change going through this repo first.

1. Source of truth

The single normative source is docs/specification.md in the zk-coins/docs repository (this site). It is complete: every key, hash, identifier, byte layout, circuit bound, proof-system parameter, wire format, Nostr event kind, Blossom endpoint, REST route, and economic rule needed to build the system is fixed there. If you believe something is still ambiguous, that is a spec bug — open a PR against docs to resolve it, then implement; do not resolve it silently in code.

2. Scope and order

The work is not finished when the node compiles. It is finished only when all four layers conform to the spec and interoperate end to end:

  1. node (Rust) — build to the spec: the single C Plonky2 circuit (spec §2.5, §1.7.9), the canonical hashing/serialization (§1.7), the Bitcoin inscription layer with NISSHAC half-aggregation and the first-occurrence scanner (§3, §1.7.10), the Nostr relay + Blossom store + ZBE (§4, §7.3, §7.4), the kernel.v1 gRPC contract (§7.8) — the kernel serves no REST and terminates no public request — and the publisher interface (§3.4, §7.6). The public REST surface of §5 / §7.5 and its capability gate belong to api, next.
  2. api — build the sole public surface: the versioned /v1/ REST contract (spec §7.5), the capability gate and challenge–response of §5.1 including the chan_bind binding to its own authoritative public host, the closed features set advertised on GET /v1/info (§6.1), NIP-05 name resolution and issuance under wallet (§4.3), and the kernel.v1 gRPC client with the error mapping of §7.8. It owns a non-value-bearing database, MUST NOT read or write the kernel's store, and MUST NOT touch Bitcoin or prove.
  3. sdk (TypeScript) — rebuild the thin typed client and account adapter against the new /v1/ API: BIP-39/32 derivation, BIP-340 signing with the sign-to-contract tweak, the transition proving handshake (/v1/txawaiting_signature/sign), and the capability/pull REST flows. The SDK is the reference for all client-side crypto (derivation, hashing, signing) — byte-equivalent at the hash and derivation level, verification-checked for signatures (§3) — and nothing more. It does not verify proofs, run scan loops, decrypt bundles, or talk to Nostr/Blossom directly; those live in the node (see "Thin-client rule" below). It talks only to the wallet's own node over REST.
  4. app (Next.js) — rebuild on top of the rewritten SDK. Migrate the app's crypto path off its in-tree WASM onto the SDK's pure-TS primitives (the SDK exists to be that single implementation), and drive the full user journey (onboard, receive, send with the fee-less publisher hand-off, balance, history, disclosure links) through the SDK against a local node. The app stays thin too: it renders what its node serves and signs with keys it holds; it does not re-implement node-side verification or scanning. The one client-side-crypto exception the spec allows is the stateless explorer applying a bearer view secret to an already-encrypted blob (spec §5) — that is presentation, not wallet trust-reduction.

Thin-client rule (hard, project-wide). zkCoins follows the Bitcoin full-node model: the wallet trusts its own node. There is no anti-node logic in the wallet/SDK/app — no client-side proof verification, no scan loops, no view-key/spend-key consistency checks against a second node, no "node integrity" UI. Anything whose purpose is to reduce trust in the node belongs node-side, or the answer is self-hosting. This rule is stated verbatim in every repo's CONTRIBUTING.md and constrains where each piece of functionality lives. The spec's client-side verification language (§2.3.3, §4.4) is always "the receiver or its node, on its behalf" — i.e. the node does it; the thin client delegates to the node it operates.

Done = node ✓ and api ✓ and sdk ✓ and app ✓, each conformant and the four proven to work together locally (§3, §4 below).

3. Definition of done

A layer is done only when all of the following hold:

  • Spec-conformant. Every normative MUST/MUST NOT in docs/specification.md that applies to the layer is implemented. The conformance test vectors (spec test-vector section) are generated and pinned (§4 below), and node and SDK each reproduce the values in their scope bit-for-bit (the SDK the hash- and derivation-level values; the circuit_digest(C) comes from the node's deterministic §1.7.9 build; the V.5/V.6 signature vectors validate by BIP-340 verification including the sign-to-contract tweak check, not byte equality).
  • NIP-17 wire-conformant (node · SDK). Sent and received events match the spec §7.3 construction and pass the V.12 wire vectors: standard kinds 0/14/13/1059/10050, independent seal-and-wrap constructions per copy, plain-text kind-14 content, no zkCoins-specific message metadata, and no unnoticed NIP-04 fallback. Discovery searches the union of name-provider relay hints and configured profile/discovery/bootstrap relays: it passes with no relays attribute and when a hint yields kind 0 while a configured relay yields kind 10050.
  • Name-survivable (node · SDK). The build passes the V.12 name-loss survivability fixture: after an established contact's name resolution completes and answers no (a 404), messaging and payment against the pinned identity still succeed, the contact is neither deleted nor re-pinned, and that name stops being presented; a lookup that merely fails to complete is inconclusive and changes nothing (spec M-16b).
  • A-to-Z tested end to end. Beyond unit coverage there is a full-journey test that exercises a real flow across all four layers running locally: create three accounts (Alice, Bob; Carol for the token-standard-2 genesis), mint, pay (with a real publisher half-aggregating the transition nullifiers and inscribing the AggregateStateNullifierV3 against a local/regtest Bitcoin), the recipient discovers + verifies + credits the coin, and a confirmation link renders. No mocks on the protocol path — real proofs, real inscriptions, real Nostr/Blossom transport. The A-to-Z suite is a machine-evaluable pass predicate, not a narrative. Fixtures (normative): Alice = the spec V.2-ext mnemonic at account' = 0, Bob = the same mnemonic at account' = 1, Carol = the same mnemonic at account' = 2; asset USD-Demo, decimals = 2, issuance_version = 1, supply 1_000_000_000 (the spec V.1/V.3 values); no publisher fee — v1 publishing is sponsored (spec D9), so no transition in this suite carries a fee coin; Bitcoin regtest with on-demand block mining; every confirmation wait is 6 mined blocks. Steps and assertions (each assertion is a hard pass/fail):
    1. Boot both nodes; GET /v1/info on each equals the pinned circuit_digests (C, C_balance) and bounds.
    2. Alice mints the asset (kind: "mint"); job reaches completed; the mint nullifier is inscribed on regtest and reaches §3.10 completed after 6 blocks; Alice's balance for the asset equals 1_000_000_000. 2b. Carol (a fresh account, send_counter = 0) genesis-mints the second asset EUR-Demo (decimals = 2, issuance_version = 2, amount = 500_000_000, cap_total = 500_000_000, terms_salt = terms_salt_fixture, spec V.4) with explicit outputs to Alice (token-standard-2 forbids self-credit in the creating transition, spec §6.5 (f)/(g)); the mint anchors; after Alice's clause-10 receive of the EUR-Demo output reaches completed, Alice's balances hold two assets, and every later assertion checks the exact full balances map (Requirement 8); Bob's USD-Demo receive (steps 3–5) must leave EUR-Demo untouched.
    3. Alice sends 250_000 to Bob (kind: "send") via a real publisher, fee-lessly (spec §7.5 presence-matrix case (c)) — the request carries no fee_address and no fee output, and a request that carries either MUST be rejected; the publisher half-aggregates and inscribes an AggregateStateNullifierV3; the wallet-side awaiting_signature recomputation (spec §7.5) passes.
    4. Bob's node discovers and verifies the coin (the §2.3.3 checks) once its nullifier is completed at 6 confirmations — the balance write happens in step 5's receive transition; Alice's balance equals 999_750_000 = 1_000_000_000 − 250_000 (change accounting exact; no fee is deducted — v1 publishing is sponsored).
    5. Bob runs the receive transition (kind: "receive", fold), self-published; it reaches completed; after it reaches completed, Bob's balance equals 250_000 (clause 7 writes balances only in the receive transition, spec §2.3.3 step 7).
    6. A confirmation link for the payment renders and reports the §3.10 state completed.
    7. Reorg control (V.9 N-09): force a 3-block regtest reorg spanning a pending nullifier; after canonical replay both nodes' (size, mth) and nav_root = Hc("NfLog/Root", size ‖ mth) (spec §3.7) equal a fresh full rescan's.
    8. Recovery control (Requirement 6): destroy Bob's node state; restore from seed + regtest chain + the artefacts served by the network's seed-discoverable relays and blob stores; Bob's balance and coin set equal the pre-destruction state. 8b. Seed-only recovery control (Requirement 13): destroy Bob's node state and his own paired relay/blob store; the rebuilt node fetches and verifies the current signed Bootstrap Manifest from one reachable node base URL (under the pinned bootstrap_pubkey), then restores from the seed and the regtest chain (the nullifier accumulator and artefact verification still require Bitcoin, §4.5 steps 2/5) via the Bootstrap Manifest's seed_relays (delivery events) and blob_stores (blob bytes); Bob's balance and coin set again equal the pre-destruction state — proving the two-plane recovery-discoverable overlap (spec §4.3/§4.5).
    9. Portability control (Requirement 10): repoint Alice's wallet to a freshly synced second node by configuration only; balances and states identical; a send from the new node succeeds.
    10. Attestation control (Requirement 9(b)): Alice produces a balance attestation for USD-Demo via POST /v1/attest/balance (spec §7.5); a fresh verifier validates it — proof, host-side anchor checks, nav_ceiling canonical — against its own scan.
    11. Grant control (Requirement 9(c)): Alice issues a view grant scoped to USD-Demo via POST /v1/grants; the grantee pulls exactly the in-scope records via the §5.1 challenge–response and cannot pull EUR-Demo records (scope clamp, spec §5.1). The suite passes iff every assertion above holds. No mocks on the protocol path — real proofs, real inscriptions, real Nostr/Blossom transport (unchanged rule above).

App layer — identity and contacts (normative)

The rules below bind the app layer and the API layer, which together own naming (Requirement 11).

Below, name means a normalized email-style identifier such as alice@example.com, resolved as a NIP-05 identity (spec §4.3).

  • Names only, everywhere. The app presents and accepts a receive identity, a payee, and a contact exclusively as a name. It MUST NOT display a raw zk1… address, a bare public key, or a bare lnurl1… string as an identity a user reads, copies, or types, and MUST NOT offer an input that accepts one. A receive QR encodes the name.
  • Setup provisions the name. Every account has a name by Requirement 11; the app is what makes that true in practice, so it MUST NOT complete account setup without one — self-hosted, operator-hosted, or from any name provider — and MUST verify it resolves to the account's own public key before declaring setup done.
  • Contacts are keyed by public key, shown by name. The stored contact record is keyed by the contact's Nostr public key and carries its current name as an attribute (spec §4.3 Known-contact storage).
  • First contact checks name consent. Before accepting a name for a counterparty, the app verifies that counterparty's name_sig (spec §4.3 Name consent), so a name reaches the user only when its seed holder attested it. On the account's own side the app produces its name_sig when the name is set or changed, without user interaction.
  • A name that stops resolving loses only its label. If a lookup completes and no longer maps the name to the pinned public key — a 404, or a different key — the app MUST keep the contact, its conversation, and its pinned payment identity, and MUST stop presenting that name. A lookup that merely fails to complete (DNS, timeout, TLS) answers nothing and changes nothing: the last confirmed name stays (spec M-16b) — the NIP-05 rule that the public key is the primary reference. It MUST NOT delete, merge, or silently re-point the contact, and MUST NOT treat this as a failure of messaging or payment where retained transport still serves.
  • Payment starts from a name or a received object. A payment is initiated only from a name, from an Invoice the payee produced, or from an existing contact. Any payment to a name the app has transacted with before runs the spec §4.3 payment-identity pin check and surfaces its warnings before the user can confirm.
  • External interoperability is an app/SDK gate. The V.12 live client matrix (spec V.12) is evaluated at this layer. A release MUST record the tested client versions and their pass/fail results; a failure attributable to zkCoins MUST block the release, while a failure reproducible against a non-zkCoins Nostr counterpart is recorded as an external defect and MUST NOT.

4. Conformance vectors come first

The spec's Poseidon-dependent values are marked <REGEN> because they must be produced, not invented (the spec explicitly forbids guessing Poseidon digests). The first node task is to implement spec §1.7.1/§1.7.2 (Poseidon-Goldilocks + the E(·) field encoding) and the §1.7.9 circuit build, then compute every <REGEN> value (the empty roots, asset_id, ash, coin.identifier, nf, the Merkle roots, and the circuit_digest(C) per network) and submit them back to docs/specification.md as a PR. Once the node produces them and the SDK's independent primitive-level re-implementation (derivation, hashing, signing) reproduces the hash- and derivation-level values bit-for-bit, those values are locked and become the conformance baseline (the SDK cross-Rust parity suite, sdk/test/cross-rust/, builds on them); the circuit_digest(C) is reachable only through the §1.7.9 circuit build and is locked by the node's deterministic build alone, and the V.5/V.6 signature vectors are locked by BIP-340 verification — including the sign-to-contract tweak check (the transition's single S2C over H(ProofData) per spec §3.2, and the half-aggregate verification of §3.3) — rather than byte equality, because BIP-340 nonce derivation includes auxiliary randomness (spec §3.2). There is deliberately no second full protocol implementation; the independent assurance a second client would provide comes from the Assurance Roadmap instead. Do not treat any <REGEN> row as authoritative until it is generated and pinned.

Build report (normative artefact). Runbook steps 1–2 also produce build-report.md in the node repository: measured gate counts and proving times at the §2.5 bounds, measured on-chain sizes at k = 1, 10, 100, max (spec §1.7.10), the per-network circuit_digest values, and the toolchain/commit identifiers. The vectors-pin PR (step 3) links it; every spec/risks reference to 'the build report' resolves here.

5. Working method: autonomous, logged, professional, consistent

Both the implementor and the agent that maintains/completes this spec work the same way:

  • Be autonomous. Do not stop to ask which option to take. When the spec leaves a genuine implementation choice (a data-structure shape, an internal module boundary, a library, a test layout), pick the option that is most professional and consistent with the rest of the system, and proceed. Run the work to completion rather than pausing for confirmation.
  • Log decisions, don't request them. Every non-obvious choice is recorded — in the relevant repo's DECISIONS.md (or a PR description / ADR), with a one-line why. The point is a durable trail, not a question. Only escalate to a human if the same issue blocks twice after genuine attempts, or if a choice would change a user-visible or architectural property in a way the spec does not cover — in which case the resolution goes into the spec first.
  • Spec changes flow through docs. If implementation reveals a spec gap or error, fix it by a PR to docs/specification.md (and regenerate any affected vectors), then implement against the corrected spec. Code and spec never diverge silently.
  • Professional & consistent is the tie-breaker. Whenever two designs are otherwise comparable, choose the one a careful engineer would defend in review: consistent naming and structure across node/sdk/app, no half-finished paths, no speculative abstraction, tests that prove behaviour rather than restate it.

Appendix A — Decisions taken while completing the spec

These are the decisions made to close the open implementation questions, recorded here so the rationale is durable. Each is now normative in docs/specification.md; the why is "professional & consistent" applied to the facts found in the node and research repos.

#DecisionWhereWhy
D1Proof system pinned to Plonky2 1.1.0 (crates.io), Goldilocks field, PoseidonGoldilocksConfig, D = 2, cyclic recursion.§1.7.9Already the working, tested choice in node; bit-stable.
D2Circuit config = standard_recursion_zk_config() (zero-knowledge on); FRI rate_bits 3 / cap_height 4 / pow 16 / 28 queries / ConstantArityBits(4,5), 100-bit FRI security.§1.7.9ZK is mandatory (proofs travel to receivers/scanners and carry nk in the witness — Req 2). The node currently uses the non-ZK config; corrected here. FRI values are the Plonky2 standard the node already runs.
D3Circuit bounds MAX_TX_INPUTS = 8, MAX_TX_OUTPUTS = 8 (outputs count recipients + change, plus the deferred fee coin if paid publishing is ever enabled); wallet splits larger transactions.§2.5Matches the node's proven MAX_IN/OUT_COINS = 8; covers a payment + change + fee with margin.
D4On-chain state nullifiers, no publisher circuit (paper model, research#16 / f392fa0): each transition publishes its account-state nullifier (Pkᵢ, Rᵢ) on Bitcoin; a publisher only NISSHAC half-aggregates the signatures (no circuit, no secret keys) into one AggregateStateNullifierV3 inscription; every node rebuilds the accumulator by first-occurrence keyed by Pkᵢ. Reverted from the batched C_batch/BatchInscription/prev_root→new_root design.§3.1–§3.6, §1.7.10, §2.2Restores contention-free self-publish and Bitcoin-only, availability-independent double-spend reconstruction; the batched root-chain could not provide all three of constant size, contention-free publish, and sound first-occurrence without a new trust base. ~64 B/tx asymptotic.
D5Canonical proof serialization = Plonky2 to_bytes() for all hashed/content-addressed proofs; bincode only for private at-rest storage.§1.7.9, §3.2Portable and stable across implementations; bincode layout is serde-version-fragile and must never enter a content address.
D6ZBE (chunked ChaCha20-Poly1305, 64 KiB chunks, counter nonce, index-binding AAD) for bundle blobs; plain NIP-44 v2 only for the small control events.§4.2.1NIP-44 v2 supports extended-length plaintext up to 2^32-1 bytes, but proof blobs belong in content-addressed Blossom transport rather than large Nostr control events. ZBE permits chunkwise authenticated processing, bounded memory, and blob-oriented framing/retry. Its AEAD is distinct: NIP-44 v2 uses ChaCha20 + HMAC-SHA-256; ZBE uses ChaCha20-Poly1305.
D7Nostr event kinds fixed: standard user metadata 0, NIP-17 message rumor 14, NIP-59 seal 13 / gift-wrap 1059, and DM relay list 10050 for identity and human messaging; zkCoins delivery rumor 1420 and ACK rumor 1421 remain payment-only; publisher profile 30421 remains addressable, with a d tag of the hex op_pubkey, and, since v1 publishing is sponsored, carries only {version, relays} — with no fee there is no fee address to bind, hence no payment-identity block and no addr_sig (D9); the paid, self-contained variant is deferred to §3.8.1; recipient payment metadata is the additive zkcoins object in kind 0, mandatory for every account the app and API layers serve and absent from an ordinary Nostr peer, with Invoice remaining the name-free path. Blossom BUD-01/02 remains under /blossom.§3.8, §7.3, §7.4Reuses the interoperable Nostr messaging/profile standards while keeping zkCoins coin delivery isolated in its own kinds and keeping both publishing and being paid free of any naming dependency (Requirement 7, Requirement 11); Blossom's SHA-256 key equals the spec's blob_id.
D8Versioned /v1/ REST API (public projection, submit+proving job handshake with wallet-held signing, capability-gated pull, publisher interface), JSON for control, canonical binary for artefacts, decimal-string big integers.§7.1, §7.5, §7.6Green field allows a clean design; keeps the proven async-job proving model from the node but reframes it onto the spec's submit/pull/publish interfaces and custody boundary.
D9Publishing is sponsored in v1; no protocol fee. A publisher pays the Bitcoin inscription cost and is not reimbursed; every external hand-off is the fee-less case (c) of the §7.5 presence matrix, accepted by publisher policy. A publisher therefore needs only an op key, a relay presence, and the HTTP endpoint its kind-30422 event names for the §7.6 hand-off — no zkCoins account, no fee address, no sk₀, no wallet, no DNS, no domain.§3.8, §7.3, §7.5, §7.6Removes the fee coin, the fee-address binding, and the four payment-identity fields the publisher profile carried only to support them — and with them the requirement that a publisher hold a wallet to sign addr_sig. Liveness never depended on a publisher: self-publish stays permissionless (§3.4), so the absence of a sponsor raises a spender's own Bitcoin cost and denies nothing. The fee-coin design is retained verbatim in §3.8.1 and deferred, not discarded.
D10nk moved out of the SPEND branch to its own hardened branch A/3' and into the operational bundle {ivk, ovk, op, nk, op_secret} held by the wallet's own node.§1.2, §6.1, §6.2The prover derives nullifiers in-circuit from the witnessed nk (§2.1 clause 4), and proving is node-side (thin-client rule) — so the prover must hold nk. It cannot spend; it can link the account's own spends, hence own-node-only. The prior "wallet only" label was unimplementable. nk is a hardened sibling of the VIEW branch, never a child or a derivative of ivk: ivk leaves the account as an irrevocable bearer capability inside zkavk (§5.8), so nk = f(ivk) would hand every holder of a history link the power to derive the account's nullifiers. The ivk-to-any-delegate / nk-to-own-node-only asymmetry of §1.2 depends on that separation.
D11Receive is a real C transition (clause 10): the creating proof is verified in-circuit (PCD across accounts), the coin admitted into the coin-history SMT and balances; MAX_RX_COINS = 4.§2.1, §2.3.3, §2.5Without it, receipts lived outside the recursion — the account's proof attested nothing about held coins, breaking §2.2's transitivity claim and any downstream verification.
D12Conditional NAV + fifth ProofData field nav_commitment: the fifth public input is a hiding commitment Hc("NavCommit", nav_root ‖ nav_rand) — where nav_root = Hc("NfLog/Root", size ‖ mth) and nav = (size, mth) — to the transition's conditional nullifier-accumulator value nav — the chain-derived accumulator value containing every dependency nullifier (opened only to a coin's recipient/disclosure verifier). Clause 1 carries it forward monotonically (prefix), and clause 10(c) forces every received coin's creating nav to be a prefix of the receiver's own — making anchoring transitive; a receiver checks nav is canonical on its own scan; reorg handling is bounded by the 6-confirmation finality directive (spec §3.9), not a no-op branch. nav_rand = HKDF("zkCoins/v1/NavRand", op_secret ‖ u64-be(send_counter)) keyed by a hardened branch A/4' = op_secret (in the operational bundle), so any node holding the bundle reproduces it (node portability) while it stays unpredictable. serialize(ProofData) = 192 bytes (ProofData is the v1 TransitionEssenceV3). (revised 2026-07-23): serialize(ProofData) is 192 bytes with the sixth npk_commit field; the nullifier accumulator is an RFC-6962 append-only Merkle log — see spec §1.4, §1.7.6, §2.1. The conditional NAV is set to the shared size_final prefix (the ≥6-confirmation-final prefix, not the live chain tip) — not a per-account minimal-covering size; v1 has no early build against still-pending dependencies, so a not-yet-final dependency means the wallet waits until it is final before building/proving (spec §2.3.2 step 5, §3.9).§1.4, §2.1, §3.9Closes the collusion-inflation hole transitively (per-hop out-of-circuit checks are not), bounds reorg finality at 6 confirmations (v1 deliberately does not adopt the paper's arbitrary-depth no-op — see Paper-Deviation Analysis D-16, issues #105/#106), and hides receive-recency (a decodable public root would leak it). op_secret is a dedicated branch so nav randomness never shares key material with the Nostr signature. Replaces the retired anchors-MMR mechanism, re-based directly onto the chain-derived nullifier accumulator (no separate structure).
D13detect_tag/epk as cleartext tags on the outer gift-wrap event (zkdt/zkepk); the rumor carries only {blob_id, blob_locators, ack_nonce}.§4.2, §4.4, §7.3The promised 1-ECDH + 1-Poseidon scan without AEAD work is impossible if the tag sits inside the NIP-44-encrypted seal. Both values are per-coin fresh and random-looking — no linkability is added.
D14out_ciphertext mechanism for ovk: per outgoing coin, K_out = HKDF("zkCoins/v1/OutKey", ovk ‖ epk) encrypts K_tx; stored in the self-delivered record.§1.3, §4.2, §5.8ovk was referenced everywhere ("recover outgoing plaintext", zkavk = ivk ‖ ovk) with no defined mechanism; the Sapling-style out-ciphertext supplies it and grounds the incoming-only (ivk, 32 B) vs full (ivk ‖ ovk, 64 B) zkavk variants.
D15The public double-spend view is on Bitcoin: the on-chain state nullifiers (Pkᵢ, Rᵢ) are the only public objects a scanner needs — there is no off-chain consensus object. ZBE applies only to the private CoinProof bundles (custody/bearer recovery), whose availability rests on indefinite retention plus the seed-discoverable relay/blob-store overlap (spec §4.3), not a fixed replica count.§3.6, §4.6, §4.2.1The retired batched design put the double-spend set in an off-chain BatchBundle, coupling admission to data availability; the paper model reconstructs it from Bitcoin alone, so replication claims are reserved for private bearer recovery only.
D16Spec wording aligned to the thin-client rule: Requirement 4's verifier is "the receiver, or its node on its behalf"; §6.2/§6.3 attribute verification to the wallet's own node; foreign-node-only wallets get fail-closed discrepancy detection, not client-side proof verification.§6.2, §6.3, §6.7The old §6.2/§6.3 text ("the wallet MUST verify against Bitcoin") contradicted this hard project rule (see "Thin-client rule" above and every repo's CONTRIBUTING.md).
D171798' is the BIP-43 purpose index, hardened, and the account root is A = m/1798'/account'.§1.2Recorded because it never was: the index and the root shape are load-bearing for recovery — the dense account scan re-derives Pk₀(n) under exactly this path (§1.2) — and every conformance chain from V.2-ext down is pinned to it. Changing either after a tagged spec invalidates pinned vectors and every derived account.
D18op stays at the leaf A/2' in v1; A/5'/j' is reserved for op rotation and unused.§1.2, §6.6, §7.7Also recorded because it never was — A/2' was the one tree position with no decision behind it. Keeping the leaf preserves the pinned V.2-ext op value in an already-tagged spec. Reserving A/5' removes the permanent foreclosure: op is the only bundle member whose compromise is active (signing, impersonation, minting view grants), and without a reserved position the only remedy is abandoning the account and moving funds. The rotation procedure is not specified in v1. Rotation would not end a former operator's ivk view, which is fixed into the address through nk_commit.

If any of these is contradicted by a hard implementation constraint, change it in the spec first, regenerate affected vectors, then implement (a change to a frozen element is a version bump, spec §1.7.8).

Path to mainnet (normative runbook)

The ordered, machine-followable sequence from first build to mainnet activation. Each step names its inputs, outputs, and a hard pass predicate; a step starts only when every earlier step's predicate holds. There is no human-gated step (project decision 2026-07-22; Assurance Roadmap) — "PR merged" steps are repository mechanics, not review gates.

#StepInputsOutputsPass predicate
1Implement §1.7 primitives + §1.7.9 circuit build (node)spec §1.7, §2node builds C and C_balance deterministically for regtest/testnet/mainnet tagstwo clean-checkout builds of the same commit produce identical circuit_digest(C) and circuit_digest(C_balance) per network tag
2Generate every <REGEN> value + the V.8 byte checksstep 1 binarythe filled V.2–V.6/V.8/V.11 values; build-report.md (measurements, §4)node reproduces every already-pinned SHA-256/Bech32m/V.8 byte exactly; all Poseidon <REGEN> cells produced; a second run reproduces them bit-for-bit
3Vectors-pin PR to docsstep 2 valuesspec with all byte-pinnable <REGEN> cells filled; F-08 status matrix filled with commit linksCI green; diff touches only vector cells + status matrix; digests in the PR equal step 1's; zero <REGEN> placeholders remain except the explicitly non-byte-pinned cells (V.5/V.6 signature values and V.12 name_sig — locked by verification; the deployment-specific block_anchor; and harness-generated V.11 boundary-suite fixtures, which mark runtime roots rather than byte-pinnable cells)
4SDK primitive paritypinned specsdk/test/cross-rust/ suiteevery V.7-parity-matrix "byte-equal" row reproduced bit-for-bit by the SDK; V.5/V.6/V.8 verification rows pass
4bFull protocol stacksteps 1–4complete node (REST §7.5, publisher §7.6, Nostr/Blossom §7.3–§7.4, kernel §7.8), SDK, appevery §7.5/§7.6 endpoint serves its normative shape against a local instance; all unit/integration suites green
5Negative controlspinned specV.9 suite in node (+SDK where in scope)every V.9 signing/encoding case (N-01–N-08, N-11–N-17) rejects with the named reason, zero accepts; N-09/N-10 (regtest reorg controls, needing a live chain) run at step 6, not here; every V.11 negative control (NL-1–NL-7) rejects; the D-05 V.11 differential-test against the independent RFC-6962 reference passes
6A-to-Z suite on regteststeps 1–5the §3 machine pass predicateevery numbered assertion of the §3 A-to-Z suite holds, including the reorg controls (N-09: canonical replay converges — the accumulator value equals a fresh full rescan's; N-10: the node detects the displacement and /health/ready stops reporting ready), restore (Req 6), and portability (Req 10) controls
7Public testnetsteps 1–6 greentestnet deployment (network tag zkCoins/v1/testnet, running against Bitcoin Signet — the maintained public test chain; the retired testnet3 MUST NOT be used); from this step the §7 wire formats are frozen (spec §1.7.8)Assurance gate "Public testnet" — all listed criteria hold; the A-to-Z journey — minus the regtest-only controls (forced reorg N-09/N-10 and on-demand mining), which remain step 6 — passes against Signet with natural block cadence
8Mainnet gate checksteps 1–7gate checklist runevery step 1–7 output is green — a machine-evaluable checklist, not a human sign-off: V.4/V.8/V.11 vectors filled and byte-equal in the V.7 parity matrix (step 4); V.9 and V.11 negative controls all reject, zero accepts (step 5); the §3 A-to-Z suite's numbered assertions all hold on regtest (step 6) and on public testnet (step 7); the D-05 V.11 differential-test against the independent RFC-6962 reference passes; remediation Gates A–C all closed (Paper-Conformance Remediation); full detail in Assurance gate "Real value (mainnet)"
9Genesis + deploymentstep 8; an operator-provisioned Bitcoin wallet funding the publisher's inscription fees (BTC UTXOs, spec §3.8); the §6.1 compose stack configurationmainnet node config: pinned digests for tag zkCoins/v1/mainnet, bitcoin_network = mainnet, SECURITY.md present in the repo with a monitored contact (an operational duty, not a review gate); the operator (a) broadcasts the genesis inscription (the network's first zkCoins nullifier inscription — the mint of the genesis/first asset, spec §2.3.1), (b) once it confirms, reads its Bitcoin block height as the pinned activation_height (spec §3.6), (c) assembles network-params.json = the pinned tuple { network_tag, circuit_digest(C), circuit_digest(C_balance), activation_height, finality_confirmations = 6, bootstrap_pubkey } in its canonical encoding (spec §3.6) and pins its SHA-256, and (d) publishes network-params.json so it is identical across all nodesGET /v1/info on the deployed node returns network = mainnet, the pinned digests, finality_confirmations = 6, and the pinned activation_height (§3.6) — identical to the published network-parameter value; the published network-params.json's SHA-256 matches the pinned digest; and /health/ready is 200 with scanner_lag = 0 after initial sync
10Mainnet activationstep 9first real-value issuance/transitionthe first mainnet transition completes the full §3 journey (mint → send → receive → confirmation link) with every §3.10 state reaching completed; the v1 freeze has been in force since the vectors pin (step 3, spec §1.7.8); from this point breaking it would strand real value

Activation is deliberately unceremonial: mainnet "activation" is nothing more than the first real transition against the mainnet network tag after step 9 — there is no switch to flip and no coordinator. Rollback before step 10 is free (nothing carries value); after step 10 a version bump additionally carries real-value migration consequences.