On August 10th, 2026, zkSecurity started an adversarial testing engagement of Ironwood, Zcash’s Lean 4 formalization of the Orchard Action circuit and of the Halo2 proving stack that verifies Orchard proofs. The engagement lasted three weeks with three consultants.
We did not focus on conventional code review, instead the primary goal of the engagement was to use mutation testing: we injected knowledge-soundness bugs into the Rust implementation and into the Lean model, regenerated the artifacts that link the two, and checked whether the formalization rejected the incorrect implementations.
The Rust implementation under test lives in the orchard and halo2 repositories. We used the revisions pinned by Ironwood’s fixture provenance file:
The engagement covers adversarial testing of the Lean formalization in zcash/ironwood against Zcash’s Ironwood circuit and Halo2 proving stack, using mutations to the circuits and to the Halo2 verifier logic, in both the Lean model and the Rust source code, to evaluate whether potential implementation bugs are correctly detected by the formal verification.
The primary target of the formalization is knowledge-soundness bugs that could enable undetectable counterfeiting. Counterfeiting is undetectable when exploitation leaves no evidence in the proof, instance, or chain history that would allow it to be identified later.
The adversarial testing exercises the two links needed to establish the formalization’s guarantees:
- Rust implementation to fixtures. We inject undetectable knowledge-soundness bugs into the Rust implementation and regenerate the fixtures. An undetectable knowledge-soundness bug that leaves the fixtures unchanged is a violation of the trust boundary the formalization relies on.
- Fixtures to proof. For any injected soundness bug that is reflected in the regenerated fixtures, we attempt to adapt the Lean model to those fixtures while keeping the specification and the critical top-level theorems fixed. If we are able to still prove the same top-level theorems, this indicates a gap in the knowledge-soundness formalization.
As part of the second category, we also look for statements in the formalization that are vacuously true, or more generally weaker than intended, and that would allow an unsound implementation to pass if only the proof is rewritten to exploit the statement’s weakness.
As bonus coverage, we also apply the first test to detectable knowledge-soundness bugs, such as query-collision bugs whose exploitation is visible in the proof, and verifier backdoors whose exploitation can be detected by replaying the proof against a corrected verifier. A detectable bug that the fixtures fail to reflect lies outside the formalization’s threat model, unless it corresponds to an actual bug in Ironwood.
The injected bugs range from missing or incorrect constraints to subtle bugs in the Halo2 verifier, including failures of adaptive soundness that let a prover exploit earlier Fiat-Shamir challenges when choosing later proof messages. The test cases were informed in part by zkSecurity’s ZK bug dataset. The exact mutation set was developed during the engagement, with the goal of broad adversarial coverage that highlights any remaining gaps in the formalization.
The scope was extended during the engagement to include Ironwood PR #215, which models Halo2’s transcript bytes, BLAKE2b, proof encoding, and verifying-key digest. These changes resolve the verification-key gate-hash gap described below.
Zero knowledge, batch verification of several proof strings, and the correctness of the deployed consensus rules outside the Action proof are explicitly not part of the scope.
Ironwood does not verify the Rust code directly, instead it relies on a re-implementation of the verifier and the Action circuit in lean. The formalization relies on generated artifacts, called fixtures, that capture what the Rust verifier does on concrete inputs, and on Lean theorems that reconstruct the same objects from the formal model. We summarize this modeling here.
- Verifier fingerprint fixtures. Orchard’s
capture_proof_fingerprint runs the real Halo2 verify_proof with a fingerprint strategy that records the assembled final multi-scalar multiplication (MSM) and the challenge schedule without evaluating the MSM. Halo2’s exporter turns the capture into a Lean file. There are four capture families: SingleAction/Honest, SingleAction/Random, MultiAction/Honest, and MultiAction/Random. For each family, the theorem nonInteractiveFingerprint_matches_derived states that Lean’s independent reassembly of the verifier MSM agrees with the capture.
- Verifying-key certificate. The theorem
vk_eq_toVerifierKey in Zcash/Snark/Keygen/Certificate.lean checks that the verifying key derived in Lean from the formal Action circuit equals the captured Rust key, including gates, query layouts, fixed and permutation commitments, lookup expressions, and domain parameters. This is used to argue equality of the lean circuit and the deployed circuit.
- Circuit specification and extraction.
Zcash/Circuits/Action/* models the Action circuit’s top-level interface. The specification ActionSpec and the extractor that recovers a witness from a satisfying trace are used to prove the Action capstones in Zcash/Snark/Capstones/Action.lean, and the ledger-level supply-integrity results.
The fingerprint captures are positive runs: honest accepting proofs, or random strings that parse and complete. They do not exercise rejection paths, and the capture stops before the final verdict, since the fingerprint strategy returns the assembled MSM and does not evaluate it.
Every experiment follows the same protocol, executed in isolated worktrees of the three repositories at their pinned revisions:
- Baseline. Confirm that the clean stack accepts the honest proof and rejects the intended counterexample.
- Implementation-only stage. Apply the Rust mutation behind an off-by-default feature flag, produce an accepting proof for a false statement where the mutation is a soundness bug, and regenerate all four fixture families. Record which families change and which stay byte-identical.
- Faithful mirroring stage. Adapt the Lean model to the mutated behavior, keeping the specification and the glue theorems unchanged, and attempt to rebuild the anchored claims against the regenerated fixtures.
- Formal endpoint. Record either a positive reconstruction that still passes (a gap), a Lean proof that the anchored claim no longer holds or cannot be packaged (a catch), or a drift correspondence showing that the certificate rejects the mutated key.
One experiment departs from this shape. The circuit mutation testing applies a batch of eleven independent constraint removals and alterations to the Action circuit and its gadgets, and reports them together, since each one runs the same protocol and reaches the same endpoint.
Each report ends with one of the following classifications:
- GAP: a demonstrated Rust soundness bug is not reflected in the regenerated fixtures (link 1 fails), or is reflected and the faithfully adapted Lean development still proves the anchored claims (link 2 fails). A defect proved directly in an audited formal claim, without a Rust mutation, is also a gap.
- NO_GAP: a demonstrated Rust soundness bug is reflected in the fixtures and the faithful Lean adaptation cannot preserve the anchored claim.
We executed 16 experiments, one of which bundles 11 common circuit bugs. Our experiments identify 6 gaps, 5 of which are known boundaries documented in the Ironwood book and code comments. The verification-key gate-hash gap was resolved by the in-scope changes in Ironwood PR #215. We note that all of the common circuit bugs are caught by the formal verification apparatus; for every bug, we prove that the circuit cannot satisfy the soundness theorem.
* These gaps are known boundaries of the security modeling, documented either in the code or the Ironwood book.
Below are listed the findings found during the engagement. High severity findings can be seen as
so-called
"priority 0" issues that need fixing (potentially urgently). Medium severity findings are most often
serious
findings that have less impact (or are harder to exploit) than high-severity findings. Low severity
findings
are most often exploitable in contrived scenarios, if at all, but still warrant reflection. Findings
marked
as informational are general comments that did not fit any of the other criteria.
Classification. GAP (known boundary), fixtures to proof.
Knowledge soundness. Undetectable. Ironwood pin 3c056cbe.
Summary.
The running time of the algorithm which recovers a witness (or break) from the action circuit does not have a limit on it’s running time.
This means, that a broken circuit can be proven “knowledge sound” for the action relation:
- The production circuit is broken.
- The Lean modelling of the circuit is correct.
- The Lean modelling of the proof system is correct.
However, the proof of extraction, ignores the circuit assignment all together and instead either:
- Brute forces a witness (so that the “extract” event holds)
- Brute forces e.g. a sinsemilla collision (so that the “break” event holds)
It is worth observing that in the formalization, there are two extractors:
- One recovering a circuit witness from an (algebraic) prover.
- One recovering a witness for the spec from a circuit witness.
We target the latter extractor, concretely:
the Action circuit computes the randomized verification key as ,
where is a witnessed scalar.
We replaced the scalar multiplication with an unconstrained Pallas point:
#[cfg(feature = "unsafe-audit-spend-authority-point-witness")]
let alpha_commitment = Point::new(
ecc_chip.clone(),
layouter.namespace(|| "witness spend authority point"),
self.spend_auth_point,
)?;
The broken circuit only enforces for an arbitrary valid point .
A prover who holds a victim’s viewing key but not the spending key can:
- Choose an independent RedPallas signing key, set to its verification key
- Compute ; without knowing .
We then regenerate the fixtures and update the Lean proofs (without changing the specifications):
instead we extract by brute force, given a ,
we write an algorithm in Lean that tries all possible values of until it finds one that satisfies the equation,
then we prove that this algorithm succeeds always.
Another simple way to illustrate this gap would be to bruteforce a collision for Sinimilla, u
and prove that one obtains a “break” instance via pigeonhole principle:
the algorithm which hashes every , eventually finds a collision.
Result.
The issue is undetectable as the honest prover could know an
and hence be indistinguishably from a prover computing as above.
Observe also that a circuit with the recently mitigated issue (an unconstrained point),
the extractor ignores the actually witnessed unconstrained point,
instead it brute-forces a witness in the intended fixed point.
Recommendation.
Model extractor efficiency part of the knowledge-soundness theorem.
We (zkSecurity) are exploring similar efforts with Caliper which allows modelling fine-grained complexity,
e.g. an extractor in “steps” and which may be of use;
more high-level interfaces/operations on top of Caliper are forthcoming.
Classification. GAP (known boundary), fixtures to proof. Adaptive knowledge soundness.
Undetectable. Ironwood pin 3c056cbe.
This experiment exercises a stronger adversarial model in which the proof-system designer
chooses protocol description after fixing a random oracle (or concrete hash function).
This is a documented boundary of the formalization.
Description.
Because the random oracle is fixed before the proof system itself is specified,
malicious proof system designers can create a proof system that is knowledge sound,
but designed to accept a set of otherwise valid proofs:
in security reductions, the random oracle has a distribution independent of the proof system,
i.e. the proof system is fixed a priori and then the “random oracle is sampled”.
However in real world deployments, the Fiat-Shamir transcript modeling is known
to designers, prior to specifying the proof system, and if a description of the proof system itself
is not used to domain separate the oracle, the designers can “program” the proof system to have “backdoored proofs”
while still proving (correctly) that the proof system is adaptive-knowledge sound.
In this experiment, we achieve this by adding a “shift” to one of the challenges:
the challenge is replaced by for a “proof system constant” .
This clearly does not affect the distribution of , it simply permutes all the values,
and we are able to formally prove that this variant of the proof system satisfies all the security properties.
However, by picking a desired , we can make a malicious proof be accepted, by setting
where is the actual challenge produced on the forged proof: we can program the proof system to accept this one invalid proof.
The example here is a very simple variant of this type of “proof system programming”,
more subtle versions may be possible, e.g. switching the order of terms in a random linear combination, after the challenges has been squeezed.
The experiment adds a compile-time scalar to
immediately after its canonical transcript squeeze in both the audit prover and verifier:
let gamma: ChallengeGamma<_> = transcript.squeeze_challenge_scalar();
let gamma = *gamma;
#[cfg(feature = "unsafe-audit-post-hoc-gamma-shift")]
let gamma = gamma + super::post_hoc_gamma_shift::<C::Scalar>();
The Lean verifier performs the same executable addition before assembling the verifier equation:
let challenges := deriveChallenges fs init ps
let challenges := addFixedGammaOffset challenges
assemble vk instanceCommitment ps challenges
For a deterministic malicious proof prefix, the first run reveals and the raw .
The verifier constant is then selected as:
Instance column 0 contains ten supplied rows, so row 10 is implicitly zero.
Its identity and permutation labels are both .
The source and target permutation factors at that row therefore become zero:
This allows us to forge a permutation check between the instance column values
and the advice columns, in the proof-of-concept we use that to spend a note with zero value,
as if it were a different note with a value of 50,000 ZEC.
Impact.
This type of programming is undetectable, programmed proofs look exactly like real proofs.
The potential impact from the existance of a programmed proof is inflation.
Recommendation.
The formalization could be strengthened to reject this specific mutation, but doing so would not resolve the general random-oracle non-instantiability boundary for the deployed system.
For future designs, consider e.g. computing a digest of a PDF which describes the scheme,
then domain separating the ROM using that digest.
To our knowledge, no deployed system does this.
There is also no evidence that the Halo2 proof system has been “programmed” in this way,
in particular, the proof system specification appear very “natural”.
Classification. NO_GAP. Knowledge soundness. Undetectable. Ironwood pin 3c056cbe.
Summary. The Action circuit derives the old nullifier storing the result in an advice cell. It then adds a copy constraint from that advice cell to the public instance cell:
layouter.constrain_instance(nf_old.inner().cell(), config.primary, NF_OLD)?;
This test investigates the following attack on the formalization: suppose we remove that copy constraint, and leave the rest of the circuit unchanged. Then, we modify the extraction function to read the private nullifier cell instead of the public instance cell. If the formalization was weaker than expected, we could still argue knowledge soundness: the extractor would extract the nullifier from the advice column, which however would not necessarily correspond to the instance value in the proof. This would be an undetectable and critical bug.
In our testing, we removed such copy constraint, and modifier the corresponding lean circuit to match the circuit fingerprint. Then, we modified the extract function to read the private nullifier cell instead of the public instance cell. Since the Spec talks about the values extracted with extract, the circuit formalization still satisfied the local FormalCircuit.Soundness property.
The formalization fails instead one level up, in the integration with the verifier extractor. It is required that the local extract function, which reads the private nullifier cell, is equal to the canonical combine function, which reads the public instance cell, and so catches this kind of mutation.
We note that we stopped modifying the Lean code to modify the extract function, which seemed to be a reasonable trust boundary. One could modify also the verifier extraction, to write a malicious extractor that reads the private nullifier cell instead of the public instance cell, but it seemed to be a more contrived attack, which could be easily caught by code inspection. We did not pursue this attack further.
Classification. NO_GAP, fixtures to proof. Knowledge soundness. Undetectable. Ironwood pin 3c056cbe.
Summary. This experiment tests the following assumption: if the key generation function from the lean side is modified to be a malicious one, the formalization should reject it. Indeed, there are some theorems around the key generation that prove some consistency properties between the derived verifying key and the circuit’s operations.
The idea of the attack would be the following: we modify the key generation function in Lean to omit a copy constraint in the circuit. The lean circuit and all the properties that rely on it are still satisfied, but the derived verifying key is not faithful to the circuit. Then, we could modify the Rust circuit to match the modified Lean circuit, and have a mismatch between the Lean circuit and the Rust circuit.
In this test, we did erase a copy constraint from the Action circuit key generation, which is the one that copies the enableSpend public instance to the advice column.
brokenCopyRaw := actionCopyRaw.erase targetRawCopy
Then we also modified the Rust circuit to match the modified Lean circuit, so that the Rust circuit does not have the copy constraint from the enableSpend public instance to the advice column. The rest of the circuit is unchanged, and the exported verifying key is checked to be consistent with the modified circuit.
The formalization correctly rejects the modified key generation. In particular, one of the consistency theorems that fails is actionCopyLink, which states that for every declared copy in the circuit, its two endpoints must belong to the same keygen permutation cycle. Modifying the keygen to omit a copy constraint breaks this property, and the theorem fails.
Classification. NO_GAP, fixtures to proof. Adaptive knowledge soundness. Detectable. Ironwood pin c4b59769.
Summary. This experiment introduces a bug in the implementation of the permutation argument. The permutation argument uses two independently squeezed challenges beta and gamma. Behind the feature unsafe-audit-verifier-beta-for-gamma, the verifier still squeezes both but substitutes beta at every gamma use site, so the transcript schedule is unchanged and only the algebra collapses:
let beta: ChallengeBeta<_> = transcript.squeeze_challenge_scalar();
let sampled_gamma: ChallengeGamma<_> = transcript.squeeze_challenge_scalar();
let gamma = if reuse_beta_for_gamma { beta.retag() } else { sampled_gamma };
An audit-only prover mirrors the substitution so honest proofs still verify. The grand product becomes ∏ (v_i + beta * sigma_i + beta) = ∏ (v_i + beta * id_i + beta), which a cheating prover can satisfy with unequal cells for every beta. On a toy circuit with a three-cell copy cycle, the values [a0, a2, a0 * a2 / a1] with a0 = 2, a1 = 1 + delta, a2 = 1 + delta^2 form such a witness: the clean MockProver rejects it, the audit prover produces a 1,248-byte proof, the mutant verifier accepts it, and the canonical verifier rejects it.
Trusted component. The verifier-fingerprint boundary and the permutation soundness argument: deriveChallenges and assemble consume beta and gamma independently, and the grand-product bridge theorem multiset_pair_eq_of_prod_eval_eq requires gamma ∉ szBadSet (linProdDiff ...) and ∀ j, beta ∉ szBadSet (pairProdDiffCoeff ... j) as separate premises.
Result. The four Orchard captures under the mutant keep 22 squeezes with distinct sampled gamma, the verifying-key blocks and the random proof bytes are identical to the release fixtures, and only the accepting MSMs change, so canonical_fingerprint_mismatch holds for all four. The faithful mirror assembleBetaForGamma with gamma := ch.beta matches all four captures. The catch is in Zcash/Snark/Experiments/VerifierBetaForGamma/DiagonalWitness.lean: on the diagonal gamma = beta, the concrete invalid witness violates at least one of the two bad-set obligations for every beta, so no independent-good-event package exists. The theorems diagonal_pair_multisets_differ, diagonal_product_collision, not_reusedChallengePairSoundness, no_independent_good_challenge_package_on_diagonal, and reused_challenge_always_outside_independent_good_event are kernel-checked with standard axioms; native_decide is confined to the eight concrete MSM comparisons.
The hypothesis that the reuse weakens Halo2 permutation soundness is confirmed for the mutant, but the gap hypothesis is not: the bad-set interface prices beta and gamma independently, so a proof-relevant collapse cannot silently retain the clean claim. Exploitation is detectable by canonical replay. The endpoint is scoped to the permutation pair-product kernel; no ledger capstone was instantiated at the toy circuit.
Classification. INFO. Ironwood pin 3c056cbe.
Summary. The typed Lean proof string is documented in Zcash/Snark/Core/ProofString.lean as never branching on a proof element’s value, with rejection of invalid encodings and points at infinity assigned to byte-level decoding outside the typed layer. In Rust, however, EqAffine::from_bytes decodes the all-zero encoding as the identity point, and Blake2bRead::read_point then rejects it during transcript absorption with the error “cannot write points at infinity to the transcript”, because Transcript::common_point requires affine coordinates:
let encoding = [0; 32];
let point = Option::<EqAffine>::from(EqAffine::from_bytes(&encoding)).expect(...);
assert!(bool::from(point.is_identity()));
On the Lean side, the single-action honest fixture’s vanishingRandom field was replaced by the group identity in an isolated module.
Trusted component. The typed post-decoding boundary between Halo2 bytes and Ironwood’s ProofString: proofStringWellFormed and assemble? in Zcash/Snark/Verifier/Assemble.lean, and the documentation claim that assemble? models every deployed rejection path that affects control flow.
Result. Lean’s assemble? accepts the identity-valued proof string and returns an MSM (identityVanishingRandomAssembly_eq_some), while the deployed reader rejects the same proof before verification. The missing predicate on the Lean side is ps.vanishingRandom ≠ 0; the well-formedness check only inspects the permutation lastEval shape before the algebraic guards. The direction is Lean-accepts, Rust-rejects, the inverse of what a gap requires, and the mutant MSM evaluates to a nonidentity point at the captured challenges (identityVanishingRandomMsm_eval_ne_zero), so it is not an accepting proof either. All four fixture families are unaffected because none contains an identity point. The two docstring claims in ProofString.lean are, however, false at the pin.
The disputed behavior is present in the unmodified stack rather than injected, so the experiment falls under the rule for potential live defects. It is not a full completeness counterexample because no honest prover was shown to emit an identity commitment.
Recommendation. Add an identity predicate on proof points to proofStringWellFormed or assemble? so that the typed model returns none where the deployed reader rejects, and correct the docstrings. A later audit should separately inspect whether any other value-dependent branch in the verifier call graph has the unsafe polarity, where Rust accepts and the typed model rejects; that question is open.
Classification. GAP (known boundary). Adaptive chosen-key transcript binding. Ironwood pin c4b59769.
Summary. Ironwood’s verifier takes the verifying-key hash as a parameter vkHash : VerifyingKey -> F rather than computing Halo2’s concrete BLAKE2b construction over the pinned key. This gap can be exploited by a malicious protocol designer and adversary as follows:
- introduce a collision in the verification-key hashing procedure. This can be done even when the hash is a random oracle, for example by using a bad serialization method or by omitting to hash certain fields of the verification key.
- the adversary runs the honest prover for the honest circuit, collecting the transcript.
- the adversary can modify the circuit to be trivially satisfied at challenges from the transcript while still having the same verification-key hash (using the collision from 1).
This experiment implements an example of such an attack behind the feature unsafe-vk-omit-gates-from-hash. The collision on vk hashes is introduced by making PinnedGates::fmt emit an empty list. As a consequence, two verification keys with different gates will have the same hash. An Orchard test freezes a single-action proof, reads a fixed polynomial evaluation c = F(x) after the challenge x, and builds a malicious circuit that is trivially satisfied at x:
original gate: P(F)
malicious gate: P(F)(F - c)
With normal hashing, a circuit containing the malicious gate alters the challenge schedule and the frozen proof is rejected. With the introduced collision (gates omitted), the malicious circuit’s hash collides with the original hash, the challenge vector and MSM are identical, and the frozen proof verifies.
Trusted component. The abstraction of the verifying-key hash, the fixed-canonical-key adaptive-statement theorem (which quantifies over one canonical key and claims no cross-key collision resistance), and vk_eq_toVerifierKey.
Result. Ironwood’s generic verifier, instantiated with a constant gateOmittingVkHash, reproduces the collision: omitted_gate_hash_collision, forged_gate_evaluations_match, forged_full_verifier_accepts, and static_full_verifier_rejects all build in Zcash/Snark/Experiments/VkHashGap/Experiment.lean. A malicious proof can be constructed by modifying the honest witness to not satisfy the target gate.
At the audited pin, the identified boundary is that Ironwood proves the verifier algebra for a supplied key and hash but does not formalize Halo2’s concrete serialization and BLAKE2b association; the concrete relation from VerifyingKey to vkHash is an external assumption. Although byte encoding, serialization, and BLAKE2b modeling were initially outside the scope and documented as model boundaries, the changes in Ironwood PR #215 were agreed to be in scope and resolve this gap.
Recommendation. Recompute the verifying-key digest in Lean and bind the serialized key description to the key derived from the Action circuit. This has been implemented in the in-scope PR #215: keyDigest recomputes the digest by hashing the serialized description of the pinned verifying key exported by Rust, and vk_eq_toVerifierKey checks that the key matches the one derived from the Lean Action circuit. Cross-key collision resistance of the reduced digest remains an external assumption.
Classification. NO_GAP, fixtures to proof. Knowledge soundness. Detectable. Ironwood pin 3c056cbe.
Summary. We re-introduce a historical halo2 vulnerability and assert that the formal verification rejects the buggy proof system. Specifically, we re-introduce the multiopen query-collision bug, fixed by halo2 PR #846. The bug is reintroduced as an off-by-default halo2 feature (unsafe-audit-query-collision) on top of the current pin, together with a narrow prover-side hook letting an audit-only “malicious prover” substitute a false claimed evaluation for one marked query.
We found the exploit does not reach any circuit gate: any gate/lookup/permutation-referenced evaluation is independently cross-checked by the vanishing argument’s aggregate expected_h_eval, itself opened via a structurally collision-immune query (an MSM object compared by pointer identity, never duplicated). This explains, at a mechanism level, why the original disclosure found “no known production circuit affected”. The working exploit instead uses a “dangling” query — an extra registration of an existing, gate-checked column at the wrapped rotation, created purely as the side effect of calling query_advice, consumed by no constraint at all. A real, gate-checked plonk::create_proof/plonk::verify_proof round trip on a minimal toy circuit accepts a proof forging that dangling query’s claimed value; the identical proof bytes are rejected by the default (fixed) build, confirming the bug is detectable by replay.
Trusted component. The SNARK-level claim under test is Ironwood’s generic verifier-acceptance predicate, Zcash.Snark.DeployedAccepts (built on assemble?, “the hypothesis every soundness endpoint consumes”) — not any Orchard-circuit-specific model. assemble? internally calls constructIntermediateSets?, guarded by hasDuplicateCommitmentPoint: a pure structural check over (commId, point) pairs, computed from the typed VerifyingKey query layout and ProofString evaluations that Lean reconstructs itself, never from a Rust-exported grouping decision.
Result. A minimal, hand-built Shape/VerifyingKey/ProofString carrying the same two-query layout and claimed evaluations (5 real, 6 forged) as the real exploit was bound directly to Lean’s assembleQueries. hasDuplicateCommitmentPoint fires on Lean’s own, completely independent reconstruction of the collision, so constructIntermediateSets? = none, assemble? = none, and DeployedAccepts’s premise is False for this trace by definition, for every URS. The path is mutated Rust prover forges the dangling query's transcript value -> plonk::verify_proof accepts (real bug) -> the identical typed data bound into Lean's assembleQueries -> hasDuplicateCommitmentPoint true -> assemble? = none -> DeployedAccepts false. No mutation was mirrored into Lean to get this result: the model catches the historical bug class on its own, because it never trusts Rust’s grouping decision in the first place.
This is a positive, NO_GAP result at the generic SNARK-verifier layer.