Skip to content

Feat/spend v3 cross tree canonical key - #23

Merged
nol4lej merged 5 commits into
mainfrom
feat/spend-v3-cross-tree-canonical-key
Oct 8, 2026
Merged

nol4lej merged 5 commits into
mainfrom
feat/spend-v3-cross-tree-canonical-key

Conversation

@nol4lej

@nol4lej nol4lej commented Oct 8, 2026

Copy link
Copy Markdown
Member

Summary

Releases circuits 0.17.0 with two rotated circuits:

  • transfer v3: spends across trees. Each input is proven against its own Merkle root (merkle_roots[0..1]), so two notes from different trees of the forest can be spent together instead of being consolidated first. 9 public inputs.
  • unshield v3: canonical spending key. Same 8 public inputs as v2.

Both versions fix a critical flaw present in every earlier spend circuit. Transfer v2 and unshield v2 must be retired on-chain as soon as v3 is active.

Security

One note admitted several nullifiers (transfer v1/v2, unshield v1/v2).

  • BabyPbk accepts any key below 2^253, but Base8 generates a subgroup of order l ≈ 2^251.6.
  • k, k + l, … therefore derive the same owner key (the same note), while each gives a different nullifier Poseidon(commitment, key).
  • Confirmed with real proofs, off-chain and on a dev node.
  • The node cannot detect it, because the key is private, so the fix lives in the circuit.

Fix

  • SpendingKeyOwner (note.circom) also requires spending_key < l.
  • Wallets already derive every key in [1, l), so no honest note is affected.

Also in v3

  • Unshield range-checks amount to u128.
  • Unshield gives recipient a constraint of its own instead of relying on the setup to bind an unused public input.

Changes

Circuits

  • SpentNote(tree_depth) in note.circom covers ownership, commitment, membership and nullifier of a spent note. Transfer uses it twice and unshield once.

  • Constraint sections are renumbered in reading order. Stale EdDSA notes and unused includes are removed.

  • Constraint counts:

    Circuit Constraints
    transfer v3 34,200
    unshield v3 17,290

Tooling

  • setup.ts verifies the ptau against the published blake2b-512 of powersOfTau28_hez_final_16.ptau, both on download and when cached.
  • With development defaults, setup.ts warns loudly. SETUP_CEREMONY=release requires SETUP_ENTROPY and a 32-byte hex SETUP_BEACON.
  • lib/rotation.ts is the single reader of ROTATE_CIRCUIT / ROTATE_VERSION, shared by build, manifest and fixtures. A typo fails instead of building over published artifacts.
  • lint:circom runs circom --inspect and fails on any unconstrained signal outside the circomlib internals it lists.

Release

  • package.json and manifest.json are at 0.17.0.
  • Manifest: transfer active 3, supported [1,2,3]; unshield active 3, supported [1,2,3].
  • v1 and v2 artifacts are unchanged.

Commits

  1. chore(scripts): shared rotation reader, verified ptau and release-mode ceremony
  2. feat!: transfer v3 across trees and unshield v3 with canonical spending key (0.17.0)
  3. chore(lint): fail lint:circom on unconstrained signals reported by circom --inspect

Tests

  • Private-input freedom scan (test/helpers/freedom.ts): with the public statement fixed, every private input is perturbed (±1, +l, 0, 2^252, 2^253−1, p−1). No input outside an explicit whitelist (the dummy slot, an absent change note) may still satisfy the circuit. Covers transfer (two real notes, two trees, one note + dummy), unshield (total, partial) and shield.
  • Spending key:
    • non-canonical aliases are refused in every slot, the dummy slot included;
    • keys 0, 1, l−1 are accepted;
    • keys l, l+1, 2^252, 2^253−1, 2^253, p−1 are refused;
    • k and l − k derive different owner keys.
  • Unshield: an amount that would wrap the conservation is refused.
  • Tooling: ptau hash check, release-ceremony guards, linter, shared rotation.
  • Every public signal is still checked to be bound by the proof.

Test plan

  • pnpm check (lint, typecheck, prettier, lint:circom with --inspect)
  • pnpm test:strict: 301 passing
  • pnpm release:verify: manifest verified against local artifacts
  • Node live e2e against these artifacts: cross-tree 42/42, spend hardening 68/68

@nol4lej
nol4lej merged commit ce833a9 into main Oct 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant