Proof-Facing Field-Domain Canonicalization Discipline

  • Name: proof-facing field-domain canonicalization discipline
  • Category: zk-integration reference / caller-boundary checklist / field-domain safety note / proof-facing object-creation discipline
  • Summary: Healthy zk integrations do not treat the proving field as a verifier implementation detail. They publish one canonical domain for every proof-facing object — nullifier, external nullifier, group id, Merkle zero value, commitment, or similar identifier — and they enforce that domain at the first caller-facing boundary where the object can become storage, uniqueness state, or user-visible protocol state. This note exists to keep the positive design target explicit instead of making readers reconstruct it only by inverting Semaphore, a16z ZkDrops, or ZK-Kit incidents.
  • Why this note exists:
    • Proof-facing bugs are often narrated as separate stories — nullifier replay, oversized poll ids, unusable Merkle trees — even though the design failure is usually one missing artifact: the protocol never made this value is a field element an end-to-end API contract.
    • Verifiers often already know the right numeric domain. The recurring failure is that caller-facing contracts, wrappers, or libraries still accept broader uint256 values and let those values become state before the verifier ever sees them.
    • The useful design habit is to review one security object all the way through: user input, storage key, object creation, proof verification, and any later payout or liveness path that depends on the same value.

Fast classifier: which proof-facing object is at risk of leaving its field domain?

  • same logical nullifier, two raw encodings — the verifier sees one field element while storage tracks raw n and n + q as different keys. Semaphore nullifier alias replay and a16z ZkDrops are the clean exemplars.
  • object created successfully, but verifier can never honor it later — the contract accepts a groupId, external nullifier, or similar identifier outside the proving field, so the application publishes live-looking state that is dead on arrival for the proof system. [[../incidents/proofs-verifiers/semaphore-groupid-external-nullifier-range-check-omission-inoperable-poll-near-miss-2022-03-07|Semaphore oversized groupId / external nullifier]] is the clean exemplar.
  • library initialized with impossible proof-path material — a tree, commitment object, or zero value becomes stored state even though later proof consumers will reject it as out of field. ZK-Kit zero-leaf tree initialization is the clean exemplar.
  • Use this note when the sharper positive-design question is where exactly should the protocol force every proof-facing value into one canonical field-safe object before it becomes state or authority?

What this note means by field-domain canonicalization

  • Field-domain canonicalization means the protocol can state, in code and in review artifacts, that a proof-facing value has one allowed numeric domain from user input to verifier semantics.
  • The useful positive-design claim is not merely the verifier will reject bad inputs. It is the contract cannot even create replay state, object state, or proof-backed library state that lies outside the proving field.
  • The design target is stronger than simple range checking. A healthy system also makes storage, replay protection, emitted events, and downstream comparisons all speak about the same canonical object the verifier reasons about.

The six joints a healthy proof-facing value should keep fused

1. Caller-facing API domain

  • The first public function that accepts a proof-facing value should already know whether the value is semantically arbitrary uint256 or field element below q.
  • If the verifier only makes sense for field elements, the public API should expose the same rule directly.
  • We check it later is usually how impossible state gets published.

2. Canonical storage representation

  • Once the protocol stores a proof-facing value, that stored form should already be the canonical verifier-side object.
  • Nullifier maps, poll registries, external-nullifier tables, and Merkle initialization state should not preserve a broader raw representation than the proving system accepts.
  • Storage should not distinguish values the verifier treats as identical.

3. Object-creation validity

  • Creating a group, tree, claim surface, or wrapper around a proof system is itself a zk security boundary.
  • Object exists onchain should already imply this object can later participate in the proof flow it was created for.
  • If creation can author unverifiable or unspendable state, the protocol has already split its truth object.

4. Proof verification semantics

  • The verifier’s field domain should be treated as protocol truth, not local verifier trivia.
  • A healthy design makes it obvious that the verifier is checking the same value family the contract accepted and stored.
  • This is where replay bugs and availability bugs stop looking different and start looking like the same missing canonicalization step.

5. Economic or operational consumer path

  • If verifier success unlocks signaling, claims, withdrawals, or object usability, the consumer path should rely on the same canonicalized value that the verifier consumed.
  • A nullifier should not be field-safe inside the proof and raw in the payout path.
  • A poll id should not be raw during creation and field-bounded only when voting starts.

6. Regression and impossibility testing

  • The protocol should prove that aliased values like x, x + q, and x + 2q cannot create distinct state or duplicate rights.
  • It should also prove that any object it can create remains usable in later proof flows.
  • The test target is not only theft resistance but impossible proof-backed objects cannot be authored at all.

What sound design looks like

  • Proof-facing values have one published machine domain
    • Reviewers should be able to point to one invariant such as nullifier < q, groupId < q, or zero < q and show that the invariant is enforced before storage or object creation.
  • Creation and consumption share the same object
    • The same canonical value should be used for verifier inputs, replay protection, group identity, emitted events, and later comparisons.
    • A healthy system does not let storage track one broader object while the verifier reasons about a narrower one.
  • Impossible proof-backed state is rejected at creation time
    • A poll, tree, or claim object should never be publishable unless the proving system could later honor it under the same assumptions.
  • Wrappers inherit the same boundary checks as base contracts
    • The caller-facing extension is often the real bug surface, even when the underlying verifier already encoded the right assumptions.
  • Tests probe the alias boundary directly
    • Good tests do not stop at proof verifies for a normal value; they also prove that nearby out-of-field encodings cannot create fresh rights or unusable objects.

Useful positive design shapes

1. Field-bounded types at the public boundary

  • The cleanest shape is a shared helper or type discipline that forces proof-facing values through one assertFieldElement(x) step before any state transition.
  • Even if Solidity ultimately stores uint256, the semantic type should be field-safe proof input, not generic integer.
  • This is the most direct answer to Semaphore- and ZkDrops-style drift.

2. Creation-time liveness guarantees

  • Group creation, poll creation, tree initialization, and similar setup paths should enforce the same field-domain guarantees that later proof verification expects.
  • The useful design standard is creation implies future proof usability, not merely creation did not revert.
  • This is the clean positive-design answer to Semaphore oversized-groupId and ZK-Kit zero-leaf incidents.

3. Canonical spent-key discipline

  • Replay protection should consume exactly the same canonical nullifier object the verifier authenticates.
  • If the protocol logs or stores proof-facing values, the logged/stored representation should already match that canonical object too.
  • The strongest shape is that there is no reachable code path where two raw encodings of one field element can produce two distinct replay keys.

4. Shared scalar-field guard libraries across extensions

  • If one protocol family has voting, signaling, airdrop, or whistleblowing wrappers around the same proving stack, all of them should reuse the same caller-boundary guards.
  • The protocol should not rely on each downstream extension remembering that groupId, scope, or nullifier stopped being a generic app identifier once it entered the proof system.

5. Object-usability invariants in tests and audits

  • Test suites should explicitly create edge-case polls, trees, and claims at the highest accepted values and prove they still admit the intended proof flow.
  • The audit question is not only can an attacker forge a proof? but also can this protocol author proof-backed state the proof system itself will later reject?

Design questions worth asking first

  • What exact proof-facing values does this contract or library accept from callers?
  • For each one, where is < q or equivalent canonicalization first enforced?
  • Does storage preserve the same object the verifier reasons about, or a broader raw encoding?
  • Can a successfully created poll, group, tree, or claim object ever be impossible to use in the later proof flow it was created for?
  • Can x and x + q ever produce distinct replay keys, identifiers, or other state even if the verifier would treat them as the same field element?
  • If an extension contract wraps the base proof system, has it inherited the same field-domain checks or silently widened the API again?

Red flags that usually mean field-domain truth is still split

  • The verifier rejects that anyway used as if verifier rejection were a safe substitute for creation-time validity
  • It's just a poll id or it's just a zero value even though the value later becomes a proof public input or Merkle-path element
  • replay maps keyed by raw uint256 or bytes32 while the proof system reasons modulo a scalar field
  • wrappers or extensions that forward proof-facing values without reusing the base contract’s scalar-field checks
  • successful object creation treated as proof of usability without any later proof-path test
  • test coverage that exercises normal inputs but never probes x + q aliases or maximum accepted ids

Failure-prevention checklist

  • Enforce the proving-field bound at the first caller-facing boundary for every proof-facing value before storage writes, object creation, replay checks, event emission, or proof verification. Verifier rejects it later is not an acceptable substitute once state can already diverge.
  • Canonicalize proof-facing values once and reuse that exact representation everywhere that matters: verifier inputs, replay-protection maps, group registries, tree metadata, emitted events, indexer-facing identifiers, and downstream payout or liveness checks.
  • Make creation-time validity carry future proof usability. A poll, group, tree, or claim surface should never be publishable unless its identifiers, zero values, and initialization material already satisfy the same field-domain assumptions the later proof flow requires.
  • Keep replay and payout logic keyed by the same canonical object the verifier authenticates. A logical nullifier, scope, or identifier must not be able to appear as two different raw keys across storage and settlement paths.
  • Reuse shared scalar-field guard helpers across wrappers, extensions, factories, and governance-added modules so the caller boundary cannot silently widen back to raw uint256 semantics one layer out from the audited core.
  • Add direct alias tests for x, x + q, and x + 2q on every replay-sensitive and availability-sensitive surface. Require those values to be rejected or normalized into one identical object rather than creating fresh rights or unusable state.
  • Test maximum accepted creation values, not only normal cases. If the protocol can create a poll, group, or tree at some edge-case identifier, the suite should prove that the resulting object still admits the intended proof flow.
  • Treat emitted identifiers and offchain consumers as part of the canonicalization boundary. Logs, indexers, and relayers should not have to infer which raw encoding was the authoritative proof-facing object.
  • During upgrades, prove the new verifier field, wrapper code, storage format, and object-creation helpers still describe the same domain object rather than quietly shifting one side of the boundary.
  • Keep a regression corpus of prior field-alias failures so future refactors re-run the exact n versus n + q, oversized-id, and out-of-field-zero-value cases that previously broke live systems.

Audit prompts

  • Where exactly is < q or equivalent canonicalization first enforced for each proof-facing value, and can any caller path reach storage, object creation, events, or proof verification before that guard runs?
  • Does storage preserve the same semantic object the verifier reasons about, or can raw uint256 / bytes32 encodings still distinguish values the proof system aliases modulo the field?
  • Can x, x + q, or x + 2q ever produce distinct replay keys, identifiers, or emitted operational truth even though the verifier would treat them as the same field element?
  • Can the protocol successfully create a poll, group, tree, or claim surface that the proving system can never later honor under ordinary use?
  • Do wrapper contracts, factories, governance-added modules, or application-specific claimers reuse the same scalar-field guards as the audited base verifier integration, or do they silently widen the public API again?
  • If a proof-facing value appears in events, indexers, relayer messages, or offchain caches, which representation is authoritative, and could two raw encodings of one logical object fork downstream operational truth?
  • Are there any places where a verifier-side rejection is relied upon as the only control even though state, uniqueness, or user-visible object creation already happened earlier?
  • During migrations or verifier swaps, where is the assertion that the new circuit field, wrapper boundary, stored identifiers, and downstream consumers still agree on one canonical object?
  • What exact test proves object created onchain already implies object remains usable in the intended proof flow later for edge-case identifiers and initialization values?
  • If a reviewer asked why one proof-facing value cannot ever become two economic rights or one dead-on-arrival object, what single canonicalization step would you point to in the code?

Lessons that should already be standard

  • The proving field is part of the public API, not a hidden verifier implementation detail. If callers can submit broader values than the proof system understands, the protocol already has two competing truths.
  • Object created successfully is not enough. For proof-backed systems, creation should already imply future proof usability under the same field-domain assumptions.
  • Replay protection only works when storage and verifier semantics share one canonical identifier. Raw-value uniqueness is meaningless when the proof system aliases modulo q.
  • Wrapper contracts are where field-domain discipline often dies. Teams remember the modulus in the audited core and forget to restate it in polls, claimers, factories, or governance-added modules.
  • Offchain consumers inherit canonicalization risk. If events expose raw proof-facing values without one authoritative encoding, indexers and relayers can fork operational truth before theft is even visible onchain.
  • Edge-case tests are not optional polish. Try x + q and try the largest accepted identifier should be permanent fixtures anywhere proof-facing values cross into storage or authority.
  • The clean design mantra is simple: one proof-facing value, one accepted domain, one stored representation, everywhere.

Negative exemplar cluster: what no field-domain canonicalization looks like

  • Semaphore nullifier alias replay preserves the clean same verifier-side nullifier, different raw replay key shape.
  • a16z ZkDrops preserves the payout-side version of the same mistake: one Merkle claim right could become two raw nullifier encodings.
  • [[../incidents/proofs-verifiers/semaphore-groupid-external-nullifier-range-check-omission-inoperable-poll-near-miss-2022-03-07|Semaphore oversized groupId / external nullifier]] preserves the object created, proof system cannot honor it later availability cousin.
  • ZK-Kit zero-leaf tree initialization preserves the library-level stored path material was never verifier-safe cousin.
  • The preserved common lesson is not merely range checks were missing. It is that contracts, libraries, and verifiers were not forced to share one canonical proof-facing object before that object became state, authority, or liveness.

How this note complements the exploit corpus

  • Start with public-input-domain-mismatch when the key question is did the contract and verifier disagree about the numeric domain of a proof-facing input?
  • Start with nullifier-canonicalization-failure when the sharper exploit-side question is did one logical nullifier reappear under two raw encodings?
  • Use this note when the sharper design question becomes what exact caller-boundary, storage, and object-creation discipline should have kept that proof-facing value canonical from the start?
  • Pair with creation-time-proof-usability-and-dead-on-arrival-object-prevention when the field-domain question is not enough because the sharper design failure is that the protocol was allowed to publish a poll, tree, or other proof-backed object before proving that object would remain usable in the later lifecycle.
  • Pair with nullifier-right-object-canonicalization-discipline when the field-domain question is not enough because the protocol also needs a sharper answer to what exact semantic one-time right should this nullifier have represented before any witness artifact was hashed into replay-prevention state?
  • Pair with proof-effect-truth-security-object when the proof-facing values are canonical but the later settlement consumer may still honor a different economically relevant effect set.
  • Pair with exact-production-artifact-attestation-and-rollback-discipline only after the proving-domain object itself is already coherent and the remaining question is whether the right verifier or circuit artifacts actually reached production.

Internal linkages

Control surface