Introduction

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:

Repository Commit Role
zcash/ironwood 3c056cbe Lean 4 / Mathlib formalization; lake build is the verification build
zcash/orchard 38bd2274 Action circuit and fingerprint capture drivers (0.15.5 plus PR #541)
zcash/halo2 cafc26e2 PLONK/IPA verifier and Lean fixture exporter (halo2_proofs 0.3.5)

Scope

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:

  1. 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.
  2. 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.

Methodology

How Ironwood binds Rust to Lean

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.

Experiment protocol

Every experiment follows the same protocol, executed in isolated worktrees of the three repositories at their pinned revisions:

  1. Baseline. Confirm that the clean stack accepts the honest proof and rejects the intended counterexample.
  2. 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.
  3. 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.
  4. 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.

Summary

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.

Experiment Classification Failing link Detectability
Exponential Circuit Assignment to Witness Extractor GAP* Fixtures to proof Undetectable
Deployed Action Challenge255 Charge Is Not Numerically Priced GAP Direct formal claim Undetectable
Final Rust MSM Evaluation Not Bound to Lean MSM Evaluation GAP* Rust to fixtures Detectable
Post-hoc Fiat-Shamir Programming GAP* Fixtures to proof Undetectable
Weak Fiat-Shamir: Instance is not Absorbed NO_GAP Fixtures to proof Unresolved
Malicious Algebraic Verifier Selected After Fixing The Random Proofs GAP* Rust to fixtures Detectable
Forgotten Output-Enable Gate NO_GAP Fixtures to proof Undetectable
Omitted Private-Nullifier to Public-Instance Copy Constraint NO_GAP None Undetectable
CommitIvk Authorization-Key Copy Omission NO_GAP Fixtures to proof Undetectable
Keygen Omission of the enableSpend Copy NO_GAP Fixtures to proof Undetectable
Existential ivk Binding in the Action Specification NO_GAP None Undetectable
Buggy Lookups: Reuse of Beta at Every Gamma Use Site NO_GAP Fixtures to proof Detectable
Identity Proof Points Rejected by Rust but Accepted by Lean INFO None N/A
Verification-Key Gate-Hash Formalization Boundary GAP* (resolved) Rust to fixtures N/A
Test Historical Bug: Multiopen Query-Collision NO_GAP Fixtures to proof Detectable
Circuit Mutation Testing NO_GAP None Undetectable

* These gaps are known boundaries of the security modeling, documented either in the code or the Ironwood book.