Creation-Time Proof Usability and Dead-on-Arrival Object Prevention

  • Name: creation-time proof usability and dead-on-arrival object prevention
  • Category: zk-integration reference / object-creation checklist / constructor-and-initializer discipline / proof-liveness design note
  • Summary: Healthy proof-backed systems do not let constructors, factories, or initializer paths publish objects that the proving system can never later use. A poll, group, tree, claim surface, or similar object should have one truth from birth: if the protocol says it exists for some proof-backed workflow, then its identifiers, defaults, cached path material, and embedded verifier-facing parameters should already satisfy the same semantic constraints the later proof flow will enforce. This note exists to keep that positive-design target explicit instead of making readers reconstruct it only by inverting created successfully, unusable later incidents like Semaphore oversized groupId polls or ZK-Kit zero-leaf tree initialization.

Why this note exists

  • A recurring class of proof-heavy failures does not forge proofs or steal funds directly. Instead, the protocol authors an object that looks live to callers but is already impossible, poisoned, or dead on arrival from the proof system’s point of view.
  • Those incidents are easy to under-rank because the verifier often behaves correctly at the end of the story. The real failure happened earlier: object creation never guaranteed future proof usability.
  • The most reusable design question is therefore not only did the verifier reject malformed inputs? but did the constructor, factory, or initializer refuse to mint impossible proof-backed state in the first place?
  • [[../incidents/proofs-verifiers/semaphore-groupid-external-nullifier-range-check-omission-inoperable-poll-near-miss-2022-03-07|Semaphore oversized groupId / external nullifier]] and ZK-Kit zero-leaf tree initialization are the clean preserved exemplars: the application could publish a poll or tree onchain even though later proof flows could never safely honor the same object.

The design claim this note is trying to preserve

  • Creation-time proof usability means object created already implies object remains usable in its intended proof-backed lifecycle under ordinary operation.
  • The useful security object is not merely the verifier call. It is the whole lifecycle object: the created poll, tree, claim surface, note registry, or group and the later proof-backed actions users expect it to support.
  • A system fails this discipline when constructors, factories, importers, migrations, or initializers accept values that make the object impossible to verify, impossible to remove from, impossible to vote with, or otherwise proof-incompatible once the later lifecycle reaches the real proving boundary.

The six joints that should stay fused

1. Object meaning at creation time

  • The creation path should know what workflow the object is being created for: voting, signaling, membership proofs, Merkle inclusion, claim redemption, withdrawal, or some other proof-backed action.
  • If the object is only valid under field-bounded ids, canonical defaults, or one verifier family, creation has to enforce those assumptions immediately.
  • We will only discover that later when someone verifies is how dead-on-arrival state gets published.

2. Embedded proof-facing parameters

  • Every proof-facing parameter embedded in the object at birth should already satisfy later verifier semantics.
  • This includes ids that later become public inputs, nullifier scopes, Merkle zero values, precomputed empty branches, cached roots, curve points, and any default or config value that can later enter witness or public-input semantics.
  • Proof-backed objects fail early when teams treat those values as harmless config at creation time and as strict proof objects only later.

3. Shared semantic domain across lifecycle stages

  • Creation, storage, proof generation, verification, and later consumption should all describe the same object under the same domain assumptions.
  • If creation accepts raw uint256, storage preserves raw uint256, and the verifier later interprets the same value as field element < q, the lifecycle object has already split.
  • A healthy system makes created object and later proof object the same semantic thing, not neighboring approximations.

4. Factory / wrapper / library inheritance

  • Many proof-backed objects are created through wrappers, extension modules, factories, or helper libraries rather than directly by the final verifier consumer.
  • The shared creation primitive is therefore a high-value control surface. If one extension forgets the same invariant, it can reintroduce impossible state above an otherwise sound base component.
  • The healthiest pattern is one shared creation-time guardrail inherited by all sibling surfaces.

5. Imported and migrated object validity

  • Dead-on-arrival state is not limited to ordinary user creation. Admin imports, migrations, upgrade adapters, and governance-created objects can bypass the same checks.
  • Any path that can materialize a proof-backed object needs the same future proof usability invariant, or the protocol will preserve one set of truths for normal creation and another for privileged maintenance paths.

6. Lifecycle tests, not just verifier tests

  • Test suites need to show the whole promise: create object -> perform ordinary proof-backed action later.
  • It is weaker to prove only verifier rejects bad inputs if impossible state can already be created and trusted before that rejection happens.
  • The right property is bad creation input never becomes live proof-backed state at all.

What sound design looks like

  • Created objects are already proof-usable objects

    • A successful constructor, initializer, or factory call should mean the object can later participate in the exact proof-backed workflow it advertises.
    • The contract should not publish objects that are syntactically live but semantically unusable.
  • Proof-facing defaults are treated as real proof material

    • Zero values, scope ids, domain separators, cached roots, precomputed empty branches, and other defaults should be reviewed as values the proof system will really consume later.
    • Defaults are not exempt from semantic constraints just because users do not see them as ordinary calldata in the later proof call.
  • The earliest shared creation primitive owns the invariant

    • The best place for the check is usually the common _create, init, or factory path rather than scattered later consumers.
    • This lets sibling modules inherit one canonical usability guarantee.
  • Impossible objects fail before they gain user-visible identity

    • The healthy system rejects bad polls before anyone can join them, bad trees before anyone can store commitments inside them, and bad claim surfaces before anyone builds automation around them.
    • Reverting late is not equivalent to rejecting creation early once the object has already become operational truth for users and integrators.
  • Lifecycle documentation names the object-level invariant explicitly

    • Reviewers and integrators should be able to read one sentence like every created group id is a verifier-safe field element or every initialized zero value is valid Merkle proof material for this tree family.
    • If the invariant only lives implicitly inside verifier code, the design boundary is too deep.

Useful positive design shapes

1. Constructor / initializer admission as a proof boundary

  • Treat every object-creation entrypoint as part of the proof security boundary, not as mere setup plumbing.
  • The question for each parameter is simple: can this value later flow into proof semantics, replay semantics, or proof-backed liveness? If yes, creation should already enforce the right domain.
  • This is the clean positive-design answer to both Semaphore oversized groupId objects and ZK-Kit zero-leaf initialization.

2. Creation implies later usability invariants

  • Encode the stronger promise directly in tests and audits: if a poll, group, tree, or claim surface exists, ordinary users should be able to use the intended proof path without discovering hidden impossible state.
  • The protocol should not rely on frontends, relayers, or offchain documentation to avoid creating semantically invalid objects.

3. Shared creation primitives across sibling modules

  • Put the invariant where every sibling feature inherits it: one _createGroup, one initTree, one claim-surface constructor, one factory helper.
  • That minimizes the risk of one extension remembering the verifier field while another silently widens back to raw transport types.

4. Stored defaults and derived ladders as first-class review objects

  • When creation parameters seed cached ladders, derived roots, scope maps, or reusable defaults, review the whole downstream blast radius rather than the initial field alone.
  • One bad seed can poison many later proofs, not just the first interaction that supplied it.

5. Migration and import parity

  • Apply the same creation-time usability checks to imported state, governance-created objects, and migration cutovers.
  • A protocol has not solved dead-on-arrival object risk if the ordinary public constructor is safe but the admin import path can still publish impossible proof-backed state.

Design questions worth asking first

  • What exact proof-backed workflow is this object supposed to support after creation?
  • Which creation-time parameters, defaults, or derived values will later become proof-facing objects, Merkle path material, public inputs, replay identifiers, or other verifier-relevant state?
  • Does successful creation already imply later proof usability, or can the first meaningful check still happen only during a later verify / remove / vote / claim call?
  • What single shared primitive owns that invariant across wrappers, factories, and sibling extensions?
  • Can imported, migrated, or admin-created objects bypass the same creation-time usability checks as public objects?
  • What test proves create object -> later ordinary proof-backed action succeeds for edge-case but valid inputs and fails early for impossible ones?

Red flags

  • The verifier catches it later.
  • This value is just configuration even though it later becomes path material, a public input, a scope, or a replay-relevant object.
  • The object was created successfully, so the proving stack must support it.
  • wrappers or extension modules that rebuild creation logic instead of inheriting one shared checked primitive.
  • migration or admin import paths that materialize proof-backed objects without replaying the same semantic validation as ordinary creation.
  • test suites that only prove later verifier reverts rather than impossible object never becomes live state.

Failure-prevention checklist

  • Make every constructor, factory, initializer, import, and migration path prove the same object-level invariant: if the protocol creates a proof-backed object, that object is immediately valid for the later proof lifecycle it advertises.
  • Identify every creation-time field and derived default that will later enter proof semantics: ids, scopes, nullifier domains, zero leaves, precomputed empty branches, cached path material, roots, commitments, or verifier-family selectors.
  • Enforce the narrow semantic domain at creation time, not only at later verification time. Reject impossible proof-backed objects before they acquire user-visible identity or dependent state.
  • Centralize usability checks in the earliest shared primitive so sibling wrappers, extensions, and factories inherit one canonical object-creation rule.
  • Treat defaults and derived ladders as first-class proof material. If a seed value will be copied or hashed into many later path elements, the creation-time check needs to cover the whole downstream object, not just the first stored field.
  • Apply the same invariant to privileged object-authoring paths: migrations, admin imports, governance-created objects, recovery tooling, and batched factory deployment scripts.
  • Add lifecycle tests that start from creation and continue through ordinary proof-backed use. Verifier rejects later is weaker than bad object cannot be created at all.
  • Add edge-case creation tests at maximum valid values and immediate out-of-domain values so the object boundary is exercised directly rather than inferred from nominal cases.
  • Document the object-level promise explicitly for integrators: what semantic type the created object must satisfy and what later proof-backed actions that promise is supposed to preserve.
  • Re-run creation-to-usability invariants after upgrades, library swaps, verifier-family changes, or migration tooling edits; dead-on-arrival state often re-enters through maintenance code rather than the original public constructor.

Audit prompts

  • What exact proof-backed workflow does this object claim to support after creation, and what invariant proves it is usable for that workflow from birth?
  • Which creation-time parameters or defaults later cross into verifier, Merkle-path, nullifier-scope, or replay semantics?
  • Could the protocol publish an object that looks live to application code but will later fail its first meaningful proof-backed action for a reason already knowable at creation time?
  • Where is the earliest shared creation primitive, and does every wrapper, factory, and sibling extension really reuse it?
  • Do admin, migration, or import paths materialize objects under the same usability constraints as public creation paths?
  • If a derived ladder, cached root, or zero default is wrong at initialization, how many later proof paths does that poison before anyone notices?
  • Are tests proving bad object rejected at creation, or only later verifier refuses to consume already-created bad object?
  • If an integrator asked what guarantees that a created poll/tree/claim surface is not dead on arrival?, what single line of code, helper, or invariant would you point to?

Lessons that should already be standard

  • Constructors and initializers are part of the proof security boundary when they create objects meant for later proof-backed workflows.
  • Verifier rejects it later is not the same guarantee as the protocol never created impossible proof-backed state.
  • Defaults are often proof material in disguise. Zero values, cached ladders, and reusable ids can be as security-critical as explicit proof calldata.
  • A proof-backed object should have one truth from birth: if it exists, it should already satisfy the semantic constraints the later proof system expects.
  • Shared creation primitives are a major safety win. The invariant belongs where every sibling feature inherits it, not where every downstream consumer tries to rediscover it.
  • Privileged imports and migrations are creation paths too. If they can publish impossible objects, the protocol still has a dead-on-arrival object problem.
  • Lifecycle tests beat local verifier tests for this class. The real promise is object usability across time, not merely late rejection at one call site.

Negative exemplar cluster

  • [[../incidents/proofs-verifiers/semaphore-groupid-external-nullifier-range-check-omission-inoperable-poll-near-miss-2022-03-07|Semaphore oversized groupId / external nullifier]] preserves the application-object version: a poll or group could be created even though later proof verification would necessarily reject the same id.
  • ZK-Kit zero-leaf tree initialization preserves the library-object version: a tree could be initialized even though later proof paths would reject the poisoned zero-derived siblings.
  • The useful common lesson is not merely range checks were missing. It is the protocol published proof-backed objects without proving those objects were usable in the proof system they were created for.

How this note complements the exploit corpus

Internal linkages

Control surface