<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Parag Mali - tag: fiat-shamir</title><description>Posts tagged fiat-shamir.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sat, 25 Jul 2026 08:57:44 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/fiat-shamir/rss.xml" rel="self" type="application/rss+xml"/><item><title>How Zero-Knowledge Broke in Real Life: Under-Constrained Circuits and the Bugs That Minted Money</title><link>https://paragmali.com/blog/how-zero-knowledge-broke-in-real-life-under-constrained-circ/</link><guid isPermaLink="true">https://paragmali.com/blog/how-zero-knowledge-broke-in-real-life-under-constrained-circ/</guid><description>Every time production zero-knowledge has publicly broken, the cryptography held. The real failures are soundness bugs in circuits, verifiers, and transcripts.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>
Zero-knowledge proofs now secure billions in rollups and bridges -- and every time one has *publicly* broken, the cryptography held. The failures were soundness bugs in the human-built artifact: a circuit missing a constraint, a verifier mishandling a zero, or a Fiat-Shamir transcript that hashed too little -- each letting a prover convince an honest verifier of something false. This is the documented record: Zcash&apos;s counterfeiting flaws, Dark Forest&apos;s range-check omission, the Frozen Heart disclosures, Solana&apos;s ZK ElGamal near-miss, the 2026 zkVM transcript breaks, the Veil Cash and FOOMCASH Groth16 exploits that actually moved money, and the adjacent BNB bridge theft. Why is this one bug family so easy to introduce, so hard to catch -- and what does the industry now build to hunt it? The through-line: the crypto held; the code didn&apos;t.
&lt;h2&gt;1. A Proof of Something That Was Not True&lt;/h2&gt;
&lt;p&gt;In June 2025, security researchers sent Solana&apos;s engineers a quiet, alarming message. The blockchain&apos;s on-chain ZK ElGamal Proof program -- the component that checks zero-knowledge proofs behind Token-2022 confidential transfers -- would accept a proof of something that was not true. The post-mortem stated the consequence plainly: a sophisticated attacker could forge a proof enabling &quot;minting unlimited tokens or draining a victim&apos;s confidential balance&quot; [@solana-june-2025].&lt;/p&gt;
&lt;p&gt;No elliptic curve had been solved. No hash had been reversed. The discrete logarithm was exactly as hard the morning after as the morning before. What went wrong was smaller and stranger than any of that. In the program&apos;s own words, &quot;a component was not included in a hash used to generate a transcript for the Fiat-Shamir Transformation&quot; [@solana-june-2025]. One algebraic value was left out of a single hash, and that omission was enough to forge a proof the verifier would wave straight through.&lt;/p&gt;

&quot;...minting unlimited tokens or draining a victim&apos;s confidential balance ... There is no known exploit.&quot; -- Solana ZK ElGamal Proof program post-mortem, June 2025
&lt;p&gt;Read the last clause of that quote carefully, because it governs everything that follows. &lt;em&gt;There is no known exploit&lt;/em&gt; [@solana-june-2025]. The bug was reported and disclosed responsibly, confidential transfers were disabled on June 11, and the proof program itself was disabled at the start of mainnet-beta epoch 805 on June 19 [@solana-june-2025]. The feature was still in testing, and no real funds moved. This was a near-miss, not a heist.&lt;/p&gt;

In a proof system, *soundness* is the guarantee that no one can produce a convincing proof of a false statement. A cheating prover cannot make an honest verifier accept a claim that is not true, except with negligible probability. Every failure in this article is a soundness failure: a valid-looking proof of something that was not so.
&lt;p&gt;That distinction -- between a proof of a true thing and a proof the verifier merely &lt;em&gt;accepts&lt;/em&gt; -- is the property under attack in every incident here. When it fails, we say the system has lost soundness.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every incident below carries a disposition tag, and the differences are load-bearing. A &lt;strong&gt;near-miss&lt;/strong&gt; was disclosed and patched with no known exploitation -- present in shipping code, never turned into stolen funds. An &lt;strong&gt;exploited-then-recovered&lt;/strong&gt; bug did move value on-chain, but the funds were returned or white-hat-rescued, so the net loss was small or zero. A &lt;strong&gt;confirmed theft&lt;/strong&gt; moved value that stayed gone. And a &lt;strong&gt;reported&lt;/strong&gt; incident is one where money moved but the zero-knowledge root cause rests on a security firm&apos;s characterization, with no primary post-mortem yet. Almost every clean zero-knowledge &lt;em&gt;circuit&lt;/em&gt; failure on record is a near-miss; every confirmed theft, as we will see, lives in a verifier, a settlement boundary, or integration code -- and the largest was an adjacent kind of bug entirely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the thesis in miniature, and it inverts the headline you would expect. Solana&apos;s flaw is not an anomaly -- it is the &lt;em&gt;signature&lt;/em&gt; of how zero-knowledge breaks in real life. Picture the system as two layers. Underneath sits a cryptographic &lt;em&gt;hardness floor&lt;/em&gt;: the discrete-log-type and pairing-based hardness assumptions -- formally argued in the generic group model -- together with the collision-resistant hashes that proofs rest on.&lt;/p&gt;
&lt;p&gt;Stacked on top is a &lt;em&gt;human-built artifact&lt;/em&gt;: the construction, the circuit, the verifier, the transcript. In every publicly disclosed failure, the floor held. The artifact above it leaked. And where that artifact guarded a rollup or a bridge, the leak was denominated in money.&lt;/p&gt;
&lt;p&gt;This piece is the documented record of that pattern -- the empirical mirror of its structural sibling, &lt;a href=&quot;https://paragmali.com/blog/a-valid-proof-of-a-false-thing-how-zero-knowledge-proofs-act/&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;A Valid Proof of a False Thing&lt;/em&gt;&lt;/a&gt;, which derives &lt;em&gt;why&lt;/em&gt; these proofs can break, and a companion to &lt;a href=&quot;https://paragmali.com/blog/never-decrypted-proving-you-ran-the-computation-without-reve/&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;Never Decrypted: Proving You Ran the Computation&lt;/em&gt;&lt;/a&gt;, which shows how the proof systems are built in the first place. Here we do neither the derivation nor the construction. We read the ledger.&lt;/p&gt;
&lt;p&gt;No cryptography broke -- so what, exactly, did? To answer that, we have to go back to the first time real money sat behind a shipping zero-knowledge system.&lt;/p&gt;
&lt;h2&gt;2. The First Silently Forgeable Proof&lt;/h2&gt;
&lt;p&gt;On March 1, 2018, Ariel Gabizon -- a cryptographer then at the Electric Coin Company, which builds &lt;a href=&quot;https://paragmali.com/blog/the-anonymity-that-actually-holds-what-zcash-and-monero-prov/&quot; rel=&quot;noopener&quot;&gt;Zcash&lt;/a&gt; -- found something disquieting in the paper Zcash had launched on. The privacy coin&apos;s proving system was a variant, known as BCTV14, of an earlier protocol called Pinocchio [@bctv-2013-879, @pinocchio-2013-279]. Gabizon discovered &quot;a subtle cryptographic flaw&quot; in it [@ecc-zcash]. Not in the hardness of the pairings the proofs relied on, but in the construction wrapped around them.&lt;/p&gt;

A **common reference string (CRS)** is a set of public parameters, produced once in a trusted setup, that both prover and verifier consult. In a pairing-based SNARK it encodes the circuit&apos;s structure as elliptic-curve group elements. If the CRS exposes elements it was supposed to withhold, a prover can turn them against the system -- which is exactly what happened to Zcash.
&lt;p&gt;The flaw was specific. Pinocchio&apos;s template deliberately left certain elements out of its public parameters; the BCTV14 variant put some of them back. Gabizon&apos;s write-up names the mechanism exactly: the construction included &quot;redundant elements in the CRS ... which are vital not to expose,&quot; and their exposure let a prover create &quot;a proof of knowledge for &lt;em&gt;any&lt;/em&gt; public input given a valid proof for &lt;em&gt;some&lt;/em&gt; public input&quot; [@gabizon-2019-119]. In plain stakes, that is counterfeiting -- forging shielded Zcash out of nothing. The assigned CVE puts the consequence in a single sentence [@cve-2019-7167].&lt;/p&gt;

&quot;...transform the proof of one statement into an ostensibly valid proof of a different statement, thereby breaking the soundness of the proof system.&quot; -- CVE-2019-7167
&lt;p&gt;The company found no evidence anyone had used it. The bug was patched silently by the Sapling network upgrade, which activated on October 28, 2018, and disclosed publicly on February 5, 2019 [@ecc-zcash].The vulnerability ran on two clocks. It was &lt;em&gt;exploitable&lt;/em&gt; for roughly two years -- from the Sprout launch in October 2016 until Sapling closed it on October 28, 2018 -- but &lt;em&gt;secret&lt;/em&gt; for only about eleven months, from Gabizon&apos;s March 2018 discovery to the February 2019 disclosure [@ecc-zcash]. A long window of exposure, a short window of concealment. The first near-miss in a long line.&lt;/p&gt;
&lt;p&gt;Zcash teaches the shape of the entire record. Separate two things that are easy to blur. Underneath any real proof system sits a &lt;strong&gt;hardness floor&lt;/strong&gt; -- named, well-studied assumptions like the discrete logarithm and elliptic-curve pairings. A pairing here is the verifier&apos;s &lt;em&gt;tool&lt;/em&gt;, the operation that checks the proof equation, not itself a hardness assumption. A Groth16-style SNARK&apos;s soundness is argued in the &lt;em&gt;generic group model&lt;/em&gt;, an idealized setting in which the adversary may only combine group elements through the allowed operations [@groth16-2016-260].&lt;/p&gt;
&lt;p&gt;On top of that floor, humans build an &lt;strong&gt;artifact&lt;/strong&gt;: the construction, the circuit, the verifier, the transcript. Gabizon&apos;s flaw did not touch the floor. It lived in the construction -- the topmost, most human layer -- and his fix confirms the diagnosis: the corrected BCTV14 is provably sound &quot;in the generic group model, when these elements are excluded from the CRS&quot; [@gabizon-2019-119]. The math was fine; the wiring around it exposed something it should not have.&lt;/p&gt;

flowchart TD
    A1[&quot;Construction: CRS and proof-system wiring&quot;] --&amp;gt; A2[&quot;Circuit: the statement compiled to field constraints&quot;]
    A2 --&amp;gt; A3[&quot;Verifier: the on-chain proof-checker&quot;]
    A3 --&amp;gt; A4[&quot;Transcript: the Fiat-Shamir challenge hash&quot;]
    A4 --&amp;gt; F1
    subgraph FLOOR[&quot;Hardness floor (generic group model) -- unbroken in every disclosed case&quot;]
        F1[&quot;Discrete-log and pairing hardness, plus collision-resistant hashes&quot;]
    end

The 0xPARC ZK Bug Tracker files Zcash under &quot;Trusted Setup Leak&quot; [@zk-bug-tracker], which sounds like toxic-waste retention -- someone kept the setup&apos;s secret trapdoor. The primary sources tell a different story. Gabizon and the Electric Coin Company describe a *redundant-CRS construction bug*: the setup published elements it should have held back, and no leaked trapdoor was needed [@gabizon-2019-119, @ecc-zcash]. Both failures live in the setup, but they are not the same failure. This article follows the primaries and keeps the two distinct.
&lt;p&gt;The same shielded-Zcash counterfeiting signature returned almost exactly eight years later, one artifact layer down. In late May 2026, Taylor Hornby, working for Shielded Labs, found an under-constrained element in the newer Orchard circuit -- the shielded pool that succeeded Sapling -- that made it &quot;possible to put arbitrary false inputs into an elliptic curve multiplication and still have the multiplication check pass&quot; [@shieldedlabs-orchard]. A complete exploit generated &quot;unlimited, undetectable counterfeit ZEC&quot; in a local regtest [@forum-zcash-orchard].&lt;/p&gt;
&lt;p&gt;The gap had been latent from Orchard&apos;s activation in May 2022 until an emergency fix on June 1, 2026 -- roughly four years -- and because Zcash&apos;s privacy hides transaction values, there is &quot;no definitive way to determine using only cryptography whether such exploitation occurred&quot; on mainnet [@forum-zcash-orchard]. A near-miss that rhymes with 2018 down to the coin, and -- as Section 3 will show -- a textbook case of a bug you cannot test for, because the flaw is a constraint that was never written.The 2026 Orchard bug sits in a different layer from the 2018 flaw -- 2018 was a redundant-CRS &lt;em&gt;construction&lt;/em&gt; bug, 2026 an under-constrained &lt;em&gt;circuit&lt;/em&gt; gadget -- but the coin and the consequence are identical: counterfeit shielded ZEC from nothing. The discovery was AI-assisted, through a targeted review of the Orchard circuit [@shieldedlabs-orchard]. It stays a near-miss: proven in regtest, never confirmed on mainnet [@forum-zcash-orchard].&lt;/p&gt;
&lt;p&gt;Two catalogs make the rest of this record legible, and both recur below. The first is the &lt;strong&gt;0xPARC ZK Bug Tracker&lt;/strong&gt;, the canonical community catalog of zero-knowledge bugs found in the wild, with its own taxonomy of failure modes [@zk-bug-tracker]. The second is the academic corpus: a 2024 systematization by Chaliasos and colleagues that analyzed 141 real-world SNARK vulnerabilities and asked what patterns they share [@chaliasos-sok-arxiv, @chaliasos-sok-usenix]. Between them, they let us treat &quot;how zero-knowledge breaks&quot; as an empirical question with an answerable shape.&lt;/p&gt;

flowchart LR
    Z[&quot;2018-19: Zcash counterfeiting -- construction&quot;] --&amp;gt; D[&quot;2021: Dark Forest range check -- circuit&quot;]
    D --&amp;gt; AZ[&quot;2022: Aztec 0-bug -- verifier&quot;]
    AZ --&amp;gt; FH[&quot;2022: Frozen Heart -- transcript&quot;]
    FH --&amp;gt; B[&quot;Oct 2022: BNB Token Hub -- adjacent Merkle verifier, CONFIRMED THEFT&quot;]
    B --&amp;gt; S[&quot;2025: Solana ZK ElGamal -- transcript, near-miss&quot;]
    S --&amp;gt; VG[&quot;Feb 2026: Veil Cash / FOOMCASH -- Groth16 verifier key, EXPLOITED then RECOVERED&quot;]
    VG --&amp;gt; V[&quot;early 2026: six zkVMs -- transcript, near-miss&quot;]
    V --&amp;gt; OR[&quot;June 4 2026: Zcash Orchard -- circuit, near-miss&quot;]
    OR --&amp;gt; AC[&quot;June 14 2026: Aztec Connect -- circuit + settlement, CONFIRMED THEFT&quot;]
    AC --&amp;gt; HK[&quot;July 3 2026: Hinkal -- verifier integration, REPORTED&quot;]
&lt;p&gt;Zcash&apos;s flaw was exotic -- a redundant element buried in a trusted setup. It would have been comforting if that were the whole story. Instead, as zero-knowledge spread from one coin to thousands of applications, the same &lt;em&gt;kind&lt;/em&gt; of bug appeared everywhere -- and in a form so ordinary it hid in plain sight.&lt;/p&gt;
&lt;h2&gt;3. The Constraint That Was Never Written&lt;/h2&gt;
&lt;p&gt;Dark Forest is a fully on-chain, real-time strategy game -- a galaxy of planets settled on Ethereum, where players keep their coordinates secret and prove in zero knowledge that each move is legal. A move requires showing that a target coordinate lies inside the map&apos;s bounds without revealing where it is. Dark Forest&apos;s circuit did this with a &lt;code&gt;RangeProof&lt;/code&gt; built on a standard library comparator [@df-blog]. The comparator looked right. It was not, and the bug was flagged by Daira Hopwood [@zk-bug-tracker].&lt;/p&gt;

An **arithmetic circuit** expresses a computation as equations -- additions and multiplications -- over the integers modulo a large prime. Compiled to a rank-1 constraint system (R1CS), it becomes a list of constraints, each of the form (linear combination) times (linear combination) equals (linear combination). A zero-knowledge proof convinces a verifier that the prover knows a *witness* -- an assignment of a value to every wire -- satisfying every constraint, while revealing nothing about it.
&lt;p&gt;The subtlety is that a proof does not certify that a statement is true. It certifies that &lt;em&gt;some&lt;/em&gt; witness satisfies the constraints that were actually written. If the constraints permit a witness the designer never intended, the proof will happily certify it.&lt;/p&gt;

A circuit is **under-constrained** when its equations fail to pin down a unique, legitimate witness -- when some assignment the designer never intended still satisfies every written constraint. The missing constraint is an *absence*: the equation that would have excluded the bad witness was simply never written.
&lt;p&gt;The smallest illustration fits in one line. Suppose a circuit constrains $y \cdot y = x$ and treats $x$ as the output. Over a prime field, that equation has two solutions for most $x$: both $y$ and its negation satisfy it. The output does not determine $y$. That non-uniqueness -- one input, more than one satisfying witness -- is the algebraic signature of every under-constrained circuit.&lt;/p&gt;
&lt;p&gt;{`
// A tiny field: the integers modulo a small prime.
const p = 23;
const mod = (a) =&amp;gt; ((a % p) + p) % p;&lt;/p&gt;
&lt;p&gt;// A constraint that only says: y * y === x  (mod p).
// It pins down x from y, but never pins down which root y is.
const x = 9;&lt;/p&gt;
&lt;p&gt;// Search the whole field for values of y that satisfy the constraint.
const witnesses = [];
for (let y = 0; y &amp;lt; p; y++) {
  if (mod(y * y) === x) witnesses.push(y);
}&lt;/p&gt;
&lt;p&gt;console.log(&apos;Constraint: y*y === &apos; + x + &apos; (mod &apos; + p + &apos;)&apos;);
console.log(&apos;Witnesses that satisfy it: &apos; + witnesses.join(&apos;, &apos;));
console.log(witnesses.length + &apos; distinct witnesses, so the output does NOT determine y.&apos;);
// Both 3 and 20 (which is -3 mod 23) pass. The circuit is under-constrained.
`}&lt;/p&gt;
&lt;p&gt;Dark Forest&apos;s bug was this same shape, dressed in a range check.&lt;/p&gt;

A **range check** forces a value to lie in a bounded interval, usually by constraining it to fit in *n* bits. Field arithmetic wraps around modulo a large prime, so without a range check a value that looks small can secretly be enormous. A large share of under-constrained bugs are missing range checks.
&lt;p&gt;Dark Forest v0.3 built its &lt;code&gt;RangeProof&lt;/code&gt; on CircomLib&apos;s &lt;code&gt;LessThan(n)&lt;/code&gt; comparator [@df-blog]. That gadget is only correct when its inputs are already known to fit in &lt;em&gt;n&lt;/em&gt; bits -- it &lt;em&gt;assumes&lt;/em&gt; the bound but does not &lt;em&gt;enforce&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;The circuit never constrained its inputs to &lt;em&gt;n&lt;/em&gt; bits, so an attacker could feed an oversized field element whose low bits looked in-range and satisfy a &lt;code&gt;RangeProof&lt;/code&gt; for an out-of-range coordinate. The fix was to add &lt;code&gt;Num2Bits&lt;/code&gt; bit-length checks on both inputs, forcing them into range before the comparison [@df-fix-commit].In Circom the assignment operators are not interchangeable. &lt;code&gt;&amp;lt;==&lt;/code&gt; both &lt;em&gt;assigns&lt;/em&gt; a value and &lt;em&gt;adds a constraint&lt;/em&gt; tying it to the equation, while &lt;code&gt;&amp;lt;--&lt;/code&gt; and &lt;code&gt;=&lt;/code&gt; only assign, producing a witness value with no constraint attached. A signal written with &lt;code&gt;&amp;lt;--&lt;/code&gt; and never separately constrained is invisible to the proof -- the classic way an intended constraint goes missing.&lt;/p&gt;
&lt;p&gt;{`
// CircomLib&apos;s LessThan(n) is only correct when BOTH inputs are &amp;lt; 2^n.
// It decides a &amp;lt; b from the low n bits. If a caller forgets to constrain
// the inputs to n bits (no Num2Bits), the gadget can be fooled.&lt;/p&gt;
&lt;p&gt;const n = 8;          // designed for values in 0..255
const bound = 100;    // we want to prove: secret &amp;lt; 100&lt;/p&gt;
&lt;p&gt;function lessThan_nbits(a, b) {
  const mask = (1 &amp;lt;&amp;lt; n) - 1;            // only the low n bits are meaningful
  return (a &amp;amp; mask) &amp;lt; (b &amp;amp; mask) ? 1 : 0;
}&lt;/p&gt;
&lt;p&gt;const honest = 42;            // a legitimate in-range coordinate
const oversized = 256 + 42;   // 298: same low 8 bits as 42, but out of range&lt;/p&gt;
&lt;p&gt;console.log(&apos;LessThan says 42 &amp;lt; 100  : &apos; + lessThan_nbits(honest, bound));    // 1, correct
console.log(&apos;LessThan says 298 &amp;lt; 100 : &apos; + lessThan_nbits(oversized, bound)); // 1, FALSE but accepted
console.log(&apos;298 is out of range, yet the range proof passes.&apos;);
console.log(&apos;The forgotten Num2Bits(8) on the input is the missing constraint.&apos;);
`}&lt;/p&gt;

flowchart TD
    IN[&quot;Attacker supplies an oversized field element as the coordinate&quot;] --&amp;gt; RP[&quot;RangeProof calls LessThan(n) on the input&quot;]
    RP --&amp;gt; ASSUME{&quot;Is the input constrained to n bits?&quot;}
    ASSUME --&amp;gt;|&quot;No Num2Bits check (the bug)&quot;| WRONG[&quot;LessThan reads only the low n bits, returns in-range&quot;]
    ASSUME --&amp;gt;|&quot;Num2Bits added (the fix)&quot;| RIGHT[&quot;Oversized input rejected before comparison&quot;]
    WRONG --&amp;gt; ACCEPT[&quot;Verifier accepts a proof for an out-of-range value&quot;]
&lt;p&gt;Now sit with why this is so hard to catch.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; You cannot unit-test the presence of a constraint you forgot to write. A test feeds the circuit an input, computes a witness, and checks that a valid proof results -- and it always does, because the illegitimate witnesses live in exactly the space the missing equation was supposed to forbid. Every positive test passes. The bug is an &lt;em&gt;absence&lt;/em&gt;, and absences do not appear in a test that only exercises what is already there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was no one-off. The same absence recurs across production libraries -- unbounded remainders, unchecked range gadgets, and more -- catalogued in the next section [@zk-bug-tracker].&lt;/p&gt;
&lt;p&gt;Different libraries, same shape: a value assigned but never pinned down. All of these were near-misses -- audit-caught or disclosed before any exploitation. But the pattern was now visible, and it was not going away.&lt;/p&gt;
&lt;p&gt;The comforting response is &quot;audit harder.&quot; Yet a missing constraint is an absence, and absences do not live in one place. To defend against them, you first have to see the whole artifact they can hide in.&lt;/p&gt;
&lt;h2&gt;4. Three Ways to Accept a Lie&lt;/h2&gt;
&lt;p&gt;If a circuit can forget a constraint, what else can? Walk up the artifact and each layer has its own way of certifying something false. There are three: the circuit, the verifier, and the transcript. Each has a signature failure, and each disclosed failure is the reason the next defense exists.&lt;/p&gt;

flowchart TD
    L1[&quot;Circuit layer: Dark Forest, Aztec nullifier, MACI -- a missing constraint or range check&quot;] --&amp;gt; L2[&quot;Verifier layer: Aztec 0-bug, BNB Merkle -- the proof-checker itself accepts a forgery&quot;]
    L2 --&amp;gt; L3[&quot;Transcript layer: Frozen Heart, Solana, zkVMs -- the Fiat-Shamir hash binds too little&quot;]
&lt;h3&gt;The circuit forgets a constraint&lt;/h3&gt;
&lt;p&gt;This is the dominant class -- the $y \cdot y = x$ intuition, generalized across production code. Once you know to look for an absence, you start to find it everywhere:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BigInt arithmetic.&lt;/strong&gt; A &lt;code&gt;BigMod&lt;/code&gt; template left its output remainder unbounded, so the &quot;remainder&quot; could exceed the modulus [@zk-bug-tracker].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circom-Pairing.&lt;/strong&gt; The library never checked the outputs of its &lt;code&gt;BigLessThan&lt;/code&gt; range checks -- Veridise&apos;s &quot;million-dollar bug caught early,&quot; found during an audit before the code reached mainnet [@veridise-circom-pairing].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MACI 1.0.&lt;/strong&gt; An under-constrained vote-validity check would have let a coordinator quietly censor targeted votes [@zk-bug-tracker].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MiMC.&lt;/strong&gt; A hash value was &quot;assigned but not constrained&quot; -- &lt;code&gt;=&lt;/code&gt; where &lt;code&gt;&amp;lt;==&lt;/code&gt; was required [@zk-bug-tracker].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ZK Email.&lt;/strong&gt; An under-constrained circuit enabled email-address spoofing [@zk-bug-tracker].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PSE, Scroll, and Polygon zkEVMs.&lt;/strong&gt; The same family surfaced as missing-constraint and unbounded-remainder findings inside production rollup circuits [@zk-bug-tracker].&lt;/li&gt;
&lt;/ul&gt;

A **nullifier** is a unique tag a zero-knowledge system publishes when a secret note is spent, so the same note cannot be spent twice. Its security depends on being deterministic and unique. If the circuit fails to pin it down, one note can produce many nullifiers, and the double-spend guard evaporates.
&lt;p&gt;The most vivid of the set is Aztec&apos;s nullifier bug. In Aztec 2.0, &quot;the field element representing this position was not actually constrained to be 32 bits,&quot; a gap that would &quot;allow the attacker to spend the same input multiple times&quot; [@aztec-disclosure]. A double-spend, produced by a single missing bit-length constraint.The 0xPARC catalog does not date its individual entries, so several years in this under-constrained cluster are approximate -- Dark Forest around 2021, the rest grouped roughly 2021 to 2023 [@zk-bug-tracker]. The dates are soft; the pattern is not.&lt;/p&gt;
&lt;p&gt;Every one of these is an absence. Every one was a near-miss -- caught in an audit or disclosed before exploitation.Not every &quot;missing range check&quot; is a soundness bug, and the distinction sharpens the point. Semaphore and its Zk-Kit dependency once shipped without a check that a proof input was smaller than the SNARK scalar-field order [@semaphore-issue-90, @zk-kit-issue-23]. But that check was in the &lt;em&gt;smart contract&lt;/em&gt;, and its absence produced &quot;a group that will always fail verification&quot; -- valid proofs &lt;em&gt;rejected&lt;/em&gt;, a group rendered unusable [@zk-bug-tracker]. That is a completeness or liveness failure -- a denial of service -- not a soundness forgery, and it was fixed by adding the missing bound [@semaphore-fix-91]. It belongs one layer up from the circuit-soundness absences in this section, and naming it that way is what keeps &quot;every failure here is a soundness failure&quot; honest.&lt;/p&gt;
&lt;h3&gt;The verifier itself is wrong&lt;/h3&gt;

The **verifier** is the code that checks a proof -- on a blockchain, usually a smart contract or precompile. It is a *separate artifact* from the circuit that produced the proof. A flawless circuit can still be defeated by a buggy verifier, because the verifier can accept proofs the circuit&apos;s math would never have permitted. Two artifacts, two independent chances to be wrong.
&lt;p&gt;Aztec&apos;s PlonK verifier, written in C++, accepted forged proofs when two proof elements were set to zero. The code recognized &lt;code&gt;0&lt;/code&gt; as &quot;not on the curve&quot; yet did not reject it, then re-interpreted that same &lt;code&gt;0&lt;/code&gt; as the point at infinity, which satisfied the final pairing equation. A point-at-infinity and zero-encoding inconsistency, living in the checker rather than in any circuit [@aztec-0bug, @zk-bug-tracker].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It is tempting to reason: &quot;the circuit was proven correct, so the system is safe.&quot; The verifier is a different program with different bugs. Aztec&apos;s circuit could have been perfect and its C++ verifier would still have accepted a forged proof. When money depends on a proof, the audited artifact must be the on-chain verifier, not only the circuit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;February 2026 then put that warning on-chain twice, in the first two known exploits against &lt;em&gt;live&lt;/em&gt; zero-knowledge verifiers. Neither touched the circuit, the pairing, or the curve. Both hit a trusted setup that was never finished.&lt;/p&gt;

Groth16, like many SNARKs, needs a **trusted setup**: a one-time ceremony that derives the proving and verification keys from secret randomness that must then be discarded. Its ceremony has two phases -- a universal &quot;powers of tau&quot; (Phase 1) and a per-circuit contribution (Phase 2) that randomizes the key parameter $\delta$. Skip Phase 2 and the tooling ships a placeholder key in which $\delta$ still equals $\gamma$ -- a valid-looking default that verifies honest proofs perfectly and accepts forged ones just as happily.
&lt;p&gt;Veil Cash, a Base privacy pool, deployed a Groth16 verifier at &lt;code&gt;0x1e65c075989189e607ddafa30fa1a0001c376cfd&lt;/code&gt; whose verification key had &lt;code&gt;delta2 == gamma2&lt;/code&gt;: both were the BN128 G2 generator point [@darknavy-veil, @veilcash-poc]. In Groth16, $\gamma$ and $\delta$ play different roles and must be independent. DARKNAVY states the consequence directly: with &lt;code&gt;delta == gamma&lt;/code&gt;, the proof element &lt;code&gt;C&lt;/code&gt; can cancel the public-input term, letting an attacker make the pairing equation pass &quot;for ANY public inputs without any witness&quot; [@darknavy-veil].&lt;/p&gt;
&lt;p&gt;Using fabricated nullifier hashes across 29 withdrawals of 0.1 ETH, an actor drained 2.9 ETH of the roughly 4.5 ETH sitting in the legacy pools [@darknavy-veil, @veilcash-poc]. The Groth16 mathematics held; the deployed verification key was wrong.&lt;/p&gt;

flowchart TD
    A[&quot;Developer runs the snarkjs setup but skips Phase 2, the delta contribution&quot;] --&amp;gt; B[&quot;Verification key ships with delta and gamma both the G2 generator placeholder&quot;]
    B --&amp;gt; C{&quot;Does the pairing check still depend on the public inputs?&quot;}
    C --&amp;gt;|&quot;delta equals gamma (the bug)&quot;| D[&quot;Pairing equation collapses into a tautology&quot;]
    D --&amp;gt; E[&quot;Attacker forges a proof for any public inputs, with no witness&quot;]
    E --&amp;gt; F[&quot;Mixer pays out: 29 forged withdrawals drain the pool&quot;]
    C --&amp;gt;|&quot;Phase 2 run, delta randomized (the fix)&quot;| G[&quot;Forged proof rejected&quot;]
&lt;p&gt;Where does an identical, on-chain-verifiable mistake come from twice in one week? zkSecurity traced the shared root cause to the tooling. Both projects used Circom with snarkjs, and in both &quot;something went extremely wrong in the setup phase ... They simply didn&apos;t have any contributions to the second phase of the setup ceremony&quot; [@zksecurity-groth16]. snarkjs initializes a new key with &lt;code&gt;gamma2&lt;/code&gt; and &lt;code&gt;delta2&lt;/code&gt; set to the same placeholder -- the G2 generator -- and expects the developer to run &lt;code&gt;snarkjs zkey contribute&lt;/code&gt;, the Phase-2 step that randomizes &lt;code&gt;delta&lt;/code&gt; while leaving &lt;code&gt;gamma&lt;/code&gt; alone.&lt;/p&gt;
&lt;p&gt;Skip that one step and the key ships with &lt;code&gt;delta = gamma = G2 generator&lt;/code&gt;, collapsing the pairing check into a tautology [@zksecurity-groth16].The bug is a missing ceremony step with a valid-looking default, which is exactly why it survives testing: honest proofs verify perfectly. zkSecurity shows the tell in one line -- run the snarkjs quick-start &lt;em&gt;without&lt;/em&gt; the contribution step and &lt;code&gt;jq &apos;.vk_gamma_2&apos;&lt;/code&gt; and &lt;code&gt;jq &apos;.vk_delta_2&apos;&lt;/code&gt; print the identical BN254 generator [@zksecurity-groth16]. A reviewer who compares those two points catches the collapse before the verifier ever holds value.&lt;/p&gt;
&lt;p&gt;Six days after Veil Cash, the identical bug was pointed at FOOMCASH, a larger &quot;ZKProof-powered&quot; lottery whose Groth16 verifier had shipped on both Ethereum and Base with &lt;code&gt;gamma&lt;/code&gt; and &lt;code&gt;delta&lt;/code&gt; set to the same point [@quillaudits-foom, @foomclub-poc].&lt;/p&gt;
&lt;p&gt;QuillAudits puts the exposure at about $1.3M in FOOM on Ethereum plus about $316K on Base -- roughly $1.6M by that accounting -- drained by algebraically forged proofs &quot;without possessing any legitimate deposits or private witness&quot;; other trackers report the gross across both chains as high as about $2.26M [@quillaudits-foom, @rekt-unfinished]. CertiK diagnosed the &lt;code&gt;delta2 == gamma2&lt;/code&gt; collapse within hours, and BlockSec flagged the event as a &quot;copycat&quot; imitation of Veil Cash -- a published post-mortem turned into an attack blueprint inside a week [@rekt-unfinished].&lt;/p&gt;
&lt;p&gt;Here the disposition tag matters as much as the mechanism, and it is the reason this cluster gets its own tier. Neither drain was a permanent theft. Veil Cash&apos;s funds were recovered in full: Decurity rescued the remaining pools, and the original actor returned the 2.9 ETH unprompted [@rekt-unfinished]. FOOMCASH was substantially white-hat-rescued -- about $1.84M, roughly 80 percent of the $2.26M gross -- and whether it was ever malicious is disputed across the primaries: zkSecurity states flatly that &quot;some websites reported the issue as an attack, which was not the case,&quot; while QuillAudits frames the same event as a drain [@zksecurity-groth16, @quillaudits-foom, @rekt-unfinished].&lt;/p&gt;
&lt;p&gt;What no source disputes is that a broken verifier moved real money on-chain. Tag both &lt;strong&gt;exploited-then-recovered&lt;/strong&gt; -- a distinct middle tier, neither a clean near-miss nor a permanent theft. And note what the cluster does to the thesis: it sharpens it. No curve broke and no Groth16 theorem failed; the deployed verification key violated the $\gamma \ne \delta$ separation the theorem assumes, so this is a verifier mismatch, not a pure in-circuit under-constraint.&lt;/p&gt;

&quot;The first two known exploits against live ZK circuits happened in the past week ... some websites reported the issue as an attack, which was not the case.&quot; -- zkSecurity, &quot;The Groth16 Trusted Setup Exploit,&quot; 2026
&lt;p&gt;On October 6, 2022, a verifier-layer failure produced the largest confirmed on-chain theft in this record -- and the largest ever traced to a proof-verification bug, one layer over from any zk-SNARK. The BNB Chain Token Hub bridge relied on a precompiled contract that verified IAVL Merkle inclusion proofs, a Cosmos-SDK proof format. As samczsun&apos;s analysis showed, the verification code validated the Merkle path against only one branch and skipped &lt;code&gt;lpath.Right&lt;/code&gt;, letting an attacker splice in an empty &quot;phantom node&quot; so that a crafted proof reproduced the expected root and certified a cross-chain deposit that had never happened [@samczsun-bnb, @emilianobonassi-bnb].&lt;/p&gt;
&lt;p&gt;The forgery minted roughly 2,000,000 BNB, nominally around $566 to $586 million; validators halted the chain before most of it escaped, limiting the funds bridged off BNB Chain to an estimated $100 million and up [@coinbase-bnb].The figure of about 2,000,000 BNB minted is consistent across write-ups; the nominal dollar value (~$566 to $586 million) and the amount successfully moved off-chain before the halt (reported between roughly $100 and $127 million) vary by source [@coinbase-bnb].&lt;/p&gt;

flowchart TD
    A[&quot;Attacker crafts a deposit that never happened&quot;] --&amp;gt; B[&quot;Builds an IAVL Merkle proof for it&quot;]
    B --&amp;gt; C[&quot;Splices in an empty phantom leaf node&quot;]
    C --&amp;gt; D{&quot;Does the verifier check both branches of the path?&quot;}
    D --&amp;gt;|&quot;Skips lpath.Right (the bug)&quot;| E[&quot;Crafted proof reproduces the expected root&quot;]
    E --&amp;gt; F[&quot;Bridge certifies the deposit, mints about 2,000,000 BNB&quot;]
    D --&amp;gt;|&quot;Checks both branches (the fix)&quot;| G[&quot;Forged proof rejected&quot;]
&lt;p&gt;That was, and remains, the &lt;em&gt;largest&lt;/em&gt; confirmed theft in this record. On June 14, 2026, a newer confirmed theft reached a zero-knowledge circuit. An attacker drained about $2.19 million from a deprecated, immutable Aztec Connect contract, &lt;code&gt;RollupProcessorV3&lt;/code&gt;, in one atomic transaction [@slowmist-aztec, @newsbtc-aztec]. SlowMist&apos;s forensic post-mortem names the mechanism a &lt;em&gt;ZK-rollup settlement-boundary bypass&lt;/em&gt; [@slowmist-aztec].&lt;/p&gt;
&lt;p&gt;The shape is worth tracing. The rollup&apos;s proof committed to 32 public-input slots, but the Layer-1 settlement contract processed only the first, indexed by an attacker-controlled &lt;code&gt;numRealTxs&lt;/code&gt; value read straight from calldata with no on-chain constraint [@slowmist-aztec, @cryptotimes-aztec]. Between the two counts sat a gap of unused slots, and SlowMist says the circuit constraint that gap slots&apos; &lt;code&gt;publicValue&lt;/code&gt; should be zero was &quot;either bypassed or not imposed&quot; [@slowmist-aztec].&lt;/p&gt;
&lt;p&gt;So the attacker packed the gap with forged deposits, minted unbacked balance across fourteen &lt;code&gt;processRollup&lt;/code&gt; calls -- seven mints, then seven withdrawals -- and cashed it out as real ETH, DAI, and wstETH [@slowmist-aztec, @cryptotimes-aztec]. SlowMist&apos;s summary is the thesis in a sentence: &quot;when the ZK circuit constraint is missing, the L1 contract layer also fails to detect the issue&quot; [@slowmist-aztec].&lt;/p&gt;

flowchart TD
    A[&quot;Attacker sets numRealTxs from calldata, no on-chain constraint&quot;] --&amp;gt; B[&quot;ZK proof commits all 32 public-input slots&quot;]
    A --&amp;gt; C[&quot;L1 settlement loop traverses only the first slot&quot;]
    B --&amp;gt; D{&quot;Circuit forces gap-slot publicValue to zero?&quot;}
    D --&amp;gt;|&quot;Either bypassed or not imposed&quot;| E[&quot;Gap slots 2..32 carry forged deposits&quot;]
    E --&amp;gt; F[&quot;Mint unbacked L2 balance across 14 processRollup calls&quot;]
    F --&amp;gt; G[&quot;Withdraw real ETH, DAI, wstETH from L1&quot;]
    D --&amp;gt;|&quot;Constraint present (the fix)&quot;| H[&quot;Gap slots forced empty, forgery rejected&quot;]

This record is non-exhaustive and still growing, so read it by category, not by count. The incident-by-incident accounting, with figures and disposition tags, is the ledger in the next section; what matters here is the shape of the conclusion: these are artifact and accounting failures, never a solved curve or a reversed hash.
&lt;p&gt;The verifier can be wrong on its own. But there is a third layer, subtler than either the circuit or the checker -- the one that turned Solana&apos;s proof into a forgery, and the one with the longest history of quietly re-shipping.&lt;/p&gt;
&lt;h3&gt;The transcript hashes too little&lt;/h3&gt;

The **Fiat-Shamir transform**, introduced by Amos Fiat and Adi Shamir in 1986, turns an interactive proof into a non-interactive one [@fiat-shamir-86]. It applies to *public-coin* protocols -- ones in which every message the verifier sends is nothing but fresh public randomness. Instead of waiting for that random challenge, the prover computes it as a hash of the conversation so far -- the *transcript*. Because the hash stands in for the verifier&apos;s coins, every public value must be fixed *before* the hash is taken; a hash is unpredictable, so the prover cannot steer it, as long as it covers everything it must.
&lt;p&gt;That last clause is the whole ballgame.&lt;/p&gt;

**Strong Fiat-Shamir** hashes the domain separator, the statement being proved, and *every* public value into the challenge. **Weak Fiat-Shamir** hashes only some of them, often just the prover&apos;s first message. The gap is the vulnerability: if a public value is left out of the hash, a malicious prover can vary that value freely, grind until the challenge is favorable, and forge a proof.

sequenceDiagram
    participant P as Malicious prover
    participant H as Fiat-Shamir hash
    Note over P,H: Weak FS hashes the commitment but omits a public value
    P-&amp;gt;&amp;gt;H: Send commitment only
    H-&amp;gt;&amp;gt;P: Challenge derived from too little
    P-&amp;gt;&amp;gt;P: Vary the omitted public value, recompute
    Note over P: Grind until the challenge suits a false statement
    P-&amp;gt;&amp;gt;H: Submit forged proof
    Note over P,H: Strong FS binds statement plus all public inputs, blocking the grind
&lt;p&gt;In April 2022, Trail of Bits published a coordinated disclosure of exactly this mistake across three different proof systems -- Girault&apos;s proof of knowledge, &lt;a href=&quot;https://paragmali.com/blog/the-discrete-log-held-the-proofs-leaked-a-field-guide-to-com/&quot; rel=&quot;noopener&quot;&gt;Bulletproofs&lt;/a&gt;, and PlonK [@tob-frozenheart-p1].&lt;/p&gt;

&quot;We&apos;ve dubbed this class of vulnerabilities Frozen Heart. The word frozen is an acronym for FoRging Of ZEro kNowledge proofs.&quot; -- Trail of Bits, April 2022
&lt;p&gt;The Bulletproofs instance was especially telling: it &quot;stem[med] from a mistake in the original academic paper,&quot; whose authors had recommended an insecure Fiat-Shamir implementation that omitted a Pedersen commitment from the hash [@tob-frozenheart-bp, @bulletproofs-2017-1066]. That is the instance the class&apos;s CVE was filed against: CVE-2022-29566 names the Bulletproofs 2017/1066 paper, which &quot;mishandles Fiat-Shamir generation because the hash computation fails to include all of the public values&quot; [@mitre-cve-2022-29566, @cve-2022-29566]. The PlonK instances shared the same shape, leaving public inputs out of the transcript [@tob-frozenheart-plonk].The pitfall was not new. Bernhard, Pereira, and Warinschi had drawn the weak-versus-strong distinction a decade earlier and shown that the weak form broke the Helios electronic-voting system [@bpw-2016-771]. The same one-line omission then recurs across four eras: Helios in 2012, Frozen Heart in 2022, Solana in 2025, and six zkVMs in 2026.&lt;/p&gt;
&lt;p&gt;This was a proactive disclosure: affected parties were notified and had remediated before publication. A near-miss, class-wide.&lt;/p&gt;
&lt;p&gt;How common is the class? A peer-reviewed systematization by Dao, Miller, Wright, and Grubbs surveyed open-source code and found 36 weak Fiat-Shamir implementations affecting 12 different proof systems [@dao-2023-691]. Not an anecdote -- a recurring omission with a name.&lt;/p&gt;
&lt;p&gt;And here the article&apos;s opening snaps shut. The Solana ZK ElGamal bug from June 2025 was this exact root cause: a component left out of the Fiat-Shamir transcript hash, following an earlier instance in the same program reported that May [@solana-june-2025, @solana-may-2025]. But the story did not stop at Solana. In early 2026 -- March, by the disclosure timeline that runs from the first report in September 2025 -- OtterSec published &quot;Unfaithful claims: breaking 6 zkVMs,&quot; reporting the &lt;em&gt;same&lt;/em&gt; omission one layer up, inside the verifiers of six production zero-knowledge virtual machines: Jolt, Nexus, Cairo-M, Ceno, Expander, and Binius64 [@osec-zkvms].&lt;/p&gt;

&quot;...public-claim data was not always bound into Fiat-Shamir transcripts before challenge generation ... We are breaking soundness in all six systems.&quot; -- OtterSec, &quot;Unfaithful claims: breaking 6 zkVMs,&quot; 2026
&lt;p&gt;The public claim the proof was &lt;em&gt;about&lt;/em&gt; -- the statement itself -- was not always hashed into the transcript before the challenge was drawn, exactly the ordering invariant the transform depends on. To make the break concrete, the researchers proved mathematically impossible statements -- a counterexample to Fermat&apos;s Last Theorem among them -- against reference implementations, noting that in a blockchain context such a forgery &quot;could translate to receiving $1M out of thin air&quot; [@osec-zkvms]. Every case was responsibly disclosed and fixed between September 2025 and March 2026, in reference and production code, with no realized on-chain theft -- a transcript-layer near-miss, six times over.&lt;/p&gt;
&lt;p&gt;Trace the thread and you get four eras of one omission -- Helios in 2012 [@bpw-2016-771], Frozen Heart in 2022 [@tob-frozenheart-p1], Solana in 2025 [@solana-june-2025], and the zkVMs in 2026 [@osec-zkvms]. The same missing line of code, re-shipped for fourteen years, each time in a more powerful system than the last.&lt;/p&gt;
&lt;p&gt;Three layers, three ways to accept a false proof -- and a pattern nagging underneath all of them. Every break we have seen has the same shape. What is it?&lt;/p&gt;
&lt;h2&gt;5. One Lens for Every Break&lt;/h2&gt;
&lt;p&gt;Line the incidents up -- Zcash, Dark Forest, Aztec, Frozen Heart, Solana, the 2026 zkVMs, and the BNB bridge next door -- and a single sentence explains all of them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Name the artifact layer -- construction, circuit, verifier, or transcript -- and you have named the break. Across the entire disclosed record, the cryptographic hardness floor never appears. No incident attacked the discrete logarithm, a pairing, or a hash. Every one attacked the human-built layer above them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the whole thesis, stated as a sorting rule. It is falsifiable -- one confirmed break of the discrete-log or pairing assumption in a shipping system would refute it -- and so far nothing in the record refutes it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is not only a reading of a handful of anecdotes. The SoK 2024 study of 141 real-world SNARK vulnerabilities found under-constrained circuits to be the single most prevalent category, and the disclosed corpus is overwhelmingly made of &lt;em&gt;soundness&lt;/em&gt; bugs rather than privacy or completeness failures [@chaliasos-sok-arxiv, @chaliasos-sok-usenix]. The pattern the incidents suggest, the data confirms. And the record is non-exhaustive by design -- new incidents keep arriving -- so what holds is the invariant, not the count: the layer explains the break, every time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You do not have to take the sorting rule on faith. Here is the ledger -- a non-exhaustive but growing record -- laid out so you can audit the claim yourself. Read the &quot;layer&quot; column and the &quot;tag&quot; column together: the clean pure-circuit cases are near-misses, the Groth16 verifier-key drains were exploited and then recovered, and every &lt;em&gt;confirmed&lt;/em&gt; theft lives in a verifier, a settlement boundary, or integration code. BNB remains the largest confirmed theft.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Artifact layer&lt;/th&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;What it enabled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Zcash counterfeiting&lt;/td&gt;
&lt;td&gt;2018-19&lt;/td&gt;
&lt;td&gt;Construction&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Forge shielded coins from a redundant CRS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark Forest RangeProof&lt;/td&gt;
&lt;td&gt;~2021&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Prove an out-of-range coordinate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aztec 2.0 nullifier&lt;/td&gt;
&lt;td&gt;~2022&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Spend the same note multiple times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circom-Pairing&lt;/td&gt;
&lt;td&gt;~2022&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Forge signatures on a pairing library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aztec PlonK &quot;0-bug&quot;&lt;/td&gt;
&lt;td&gt;~2022&lt;/td&gt;
&lt;td&gt;Verifier&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Accept forged proofs via a zero encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frozen Heart (Bulletproofs, PlonK)&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;Transcript&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Forge proofs for false statements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana ZK ElGamal&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;Transcript&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Mint unlimited tokens or drain balances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Six zkVMs (Jolt, Nexus, Cairo-M, Ceno, Expander, Binius64)&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Transcript&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Forge proofs by an unbound public claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zcash Orchard counterfeiting&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Near-miss&lt;/td&gt;
&lt;td&gt;Forge unlimited shielded ZEC via an under-constrained EC-mult gadget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BNB Token Hub&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;Verifier (Merkle, adjacent)&lt;/td&gt;
&lt;td&gt;Confirmed theft&lt;/td&gt;
&lt;td&gt;Mint about 2,000,000 BNB via a phantom node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Veil Cash&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Verifier (trusted setup)&lt;/td&gt;
&lt;td&gt;Exploited, recovered&lt;/td&gt;
&lt;td&gt;Forge Groth16 withdrawals after &lt;code&gt;delta2 == gamma2&lt;/code&gt;, drain 2.9 ETH, returned in full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOOMCASH&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Verifier (trusted setup)&lt;/td&gt;
&lt;td&gt;Exploited, recovered&lt;/td&gt;
&lt;td&gt;Copycat Groth16 &lt;code&gt;gamma == delta&lt;/code&gt;, about $2.26M gross, about 80 percent white-hat-recovered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aztec Connect&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Circuit + settlement&lt;/td&gt;
&lt;td&gt;Confirmed theft&lt;/td&gt;
&lt;td&gt;Mint unbacked L2 balance through gap slots whose zero constraint was either bypassed or not imposed, drain about $2.19M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hinkal&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Verifier integration&lt;/td&gt;
&lt;td&gt;Reported&lt;/td&gt;
&lt;td&gt;Reported proofless deposit accepted without a ZK proof, about $820K laundered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The money framing needs the same care. The artifact is what leaked, and where it guarded a rollup, a pool, or a bridge, the leak was measured in funds minted or drained. &quot;The bugs that minted money&quot; is discharged by reading the ledger &lt;em&gt;by category&lt;/em&gt;, not by count: the permanent thefts are BNB Token Hub -- still the &lt;em&gt;largest&lt;/em&gt; -- and Aztec Connect [@coinbase-bnb, @slowmist-aztec]; the Veil Cash and FOOMCASH verifier-key drains were exploited-then-recovered, not permanent theft [@darknavy-veil, @quillaudits-foom, @rekt-unfinished]; and Hinkal is &lt;em&gt;reported&lt;/em&gt;, its &quot;proofless deposit&quot; root cause resting on a security firm&apos;s reading with a full post-mortem still to follow, its loss put at roughly $800,000 to $830,000, so this article uses about $820,000 [@dextools-hinkal, @cryptotimes-hinkal].&lt;/p&gt;
&lt;p&gt;Every one reinforces the scoped claim rather than disturbing it: each is a verifier, settlement, trusted-setup, or integration mismatch. What the record still shows &lt;em&gt;no&lt;/em&gt; confirmed theft of is the pure case -- an in-circuit under-constraint &lt;em&gt;independent of&lt;/em&gt; any settlement, verifier, setup, or integration mismatch. That is not luck to rely on; it is a fact about the record, and the record is still short.&lt;/p&gt;
&lt;p&gt;A lens that names every break is worth little if you cannot act on it. So the field built tools -- one for each layer. How well do they work?&lt;/p&gt;
&lt;h2&gt;6. How the Industry Hunts the Bug Now&lt;/h2&gt;
&lt;p&gt;Each defense in use today is a scar -- a direct response to a documented failure. Read them from most sound to most human, and keep one tension in view the whole way down. The tools that can &lt;em&gt;prove&lt;/em&gt; a circuit is properly constrained cannot yet run on production-size circuits, and the tools that run on production-size circuits cannot prove anything. No deployed tool is both. Call it the sound-versus-scalable frontier.&lt;/p&gt;
&lt;h3&gt;Sound uniqueness verification&lt;/h3&gt;
&lt;p&gt;The flagship family targets the dominant bug class head-on.&lt;/p&gt;

**Formal verification** of a circuit proves a property holds for *all* possible inputs, rather than checking a few. For under-constraint, the property is *uniqueness*: every output signal is determined by the inputs, so no second witness exists. A sound tool either proves uniqueness or hands back a concrete counterexample -- two distinct witnesses for the same input, the $y \cdot y = x$ signature made mechanical.
&lt;p&gt;The lineage runs from a first sketch to increasingly capable engines. &lt;strong&gt;Ecne&lt;/strong&gt; was the first widely known open checker that R1CS outputs are uniquely determined by inputs [@ecne-github, @ecne-writeup].Ecne carries a caveat that sharpens what &quot;sound&quot; means: it is a &lt;em&gt;partial&lt;/em&gt; prover. A &lt;code&gt;false&lt;/code&gt; result &quot;does not mean that the constraints aren&apos;t sound, it just means that Ecne is unable to prove&quot; they are [@ecne-github]. Picus, CIVER, and ConsCS go further, returning an actual witness pair.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Picus / QED2&lt;/strong&gt; made it quantitative, pairing an SMT solver with uniqueness inference to either prove a signal is determined or return a two-witness exploit; it resolved about 70 percent of a 163-circuit Circom benchmark and found 8 previously unknown vulnerabilities [@picus-2023-512, @picus-github, @pldi23-picus]. &lt;strong&gt;CIVER&lt;/strong&gt; checks uniqueness compositionally, verifying a gadget once and reusing it, from inside the Circom compiler [@civer-github]. &lt;strong&gt;ConsCS&lt;/strong&gt; is the current accuracy leader on Circom, lifting the solved rate above 80 percent [@conscs-polyu]. And &lt;strong&gt;halo2-analyzer&lt;/strong&gt; (also called korrekt) carries the whole approach beyond R1CS to Plonkish and Halo2 circuits [@halo2-analyzer-eprint, @halo2-analyzer-github].&lt;/p&gt;
&lt;p&gt;The governing bound is negative and structural. What these tools actually decide is &lt;em&gt;non-uniqueness&lt;/em&gt;: they search for a second, distinct witness that satisfies the same constraints on the same input, and that witness &lt;em&gt;pair&lt;/em&gt; is the certificate. Finding one is an NP search, because R1CS satisfiability is itself NP-complete -- so a sound checker cannot also be complete and efficient on every circuit, barring $P = NP$ [@picus-2023-512].&lt;/p&gt;
&lt;p&gt;Deciding &lt;em&gt;uniqueness itself&lt;/em&gt; is harder still -- it asks both that a witness exists and that no second one does, which places it above NP, in the class of differences of NP sets; the canonical Unique-SAT problem there is complete under randomized reductions [@papadimitriou-yannakakis-1984, @blass-gurevich-1982, @valiant-vazirani-1986]. Either way the conclusion is the same and unavoidable: every tool in this family is &lt;em&gt;sound but incomplete&lt;/em&gt;. It may answer &quot;unknown&quot; or time out. That is not an implementation gap; it is the shape of the problem.&lt;/p&gt;

flowchart LR
    subgraph SOUND[&quot;Sound: a clean result proves absence&quot;]
        S1[&quot;Ecne, Picus, CIVER, ConsCS, halo2-analyzer -- run on gadgets&quot;]
    end
    subgraph SCALE[&quot;Scalable: runs on hundreds of production circuits&quot;]
        S2[&quot;ZKAP, Circomspect -- lint, but no proof of absence&quot;]
    end
    S1 -. &quot;no tool sits in both boxes&quot; .-&amp;gt; S2
&lt;h3&gt;Scalable static analysis&lt;/h3&gt;
&lt;p&gt;When you cannot prove, you can still scan. &lt;strong&gt;ZKAP&lt;/strong&gt; implements nine detectors over a &lt;em&gt;circuit dependence graph&lt;/em&gt; and ran across 258 circuits from popular Circom projects, surfacing previously unknown vulnerabilities [@zkap-2023-190, @zkap-usenix]. Trail of Bits&apos; &lt;strong&gt;Circomspect&lt;/strong&gt; is a linter that flags the tell-tale patterns -- a signal assigned with &lt;code&gt;&amp;lt;--&lt;/code&gt; but never constrained with &lt;code&gt;&amp;lt;==&lt;/code&gt;, or an output left unconstrained [@circomspect]. Both scale to hundreds of circuits, and both are &lt;em&gt;unsound&lt;/em&gt;: a clean scan means &quot;no known-bad pattern found,&quot; not &quot;no bug.&quot;&lt;/p&gt;
&lt;h3&gt;Prevention and the human net&lt;/h3&gt;
&lt;p&gt;Two more defenses sit at the ends of the spectrum. At the preventive end, &lt;strong&gt;strong-Fiat-Shamir transcript libraries&lt;/strong&gt; bind the domain separator, the statement, and every public input by construction, removing the avoidable Frozen Heart class before it can be written. The 2026 zkVM breaks named the state-of-practice version precisely: &lt;em&gt;merge the proof and transcript&lt;/em&gt; so every value the prover sends is absorbed before any challenge is drawn -- &quot;Halo2 follows this pattern, and Binius is transcript-centric as well,&quot; though even transcript-centric Binius still missed binding the statement [@osec-zkvms].&lt;/p&gt;
&lt;p&gt;Alongside it sits a newer, humbler discipline: &lt;strong&gt;trusted-setup ceremony hygiene as a shipped step&lt;/strong&gt; -- confirming the deployed verification key actually finished Phase 2, with $\delta$ independent of $\gamma$, before it holds value. That is prevention by checklist, not a formal tool, but it is exactly the gap Veil Cash and FOOMCASH walked through, and a one-line check would have closed it [@zksecurity-groth16].&lt;/p&gt;
&lt;p&gt;At the human end sit &lt;strong&gt;specialist audits and bug bounties&lt;/strong&gt; -- the last net, and the one that actually caught the headline cases. The Solana report arrived through disclosure; so did the BNB analysis and the 2026 Groth16 drains [@solana-june-2025, @rekt-unfinished]. Firms like Veridise operationalize the sound branch commercially, folding uniqueness checkers into human audits of Circom, Halo2, and other circuits [@veridise-audits].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Run a fast linter (Circomspect, ZKAP) on every commit to catch the obvious absences [@circomspect]. Reserve sound uniqueness checkers (Picus) for the small, high-value gadgets where a proof of absence justifies the runtime [@picus-github]. Before deployment, confirm the trusted setup actually finished -- that the verification key&apos;s $\delta$ is not still equal to $\gamma$ [@zksecurity-groth16]. Commission a specialist audit before mainnet, and keep a bounty live afterward -- it is how the Solana bug surfaced [@solana-june-2025].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So we have sound tools that do not scale and scalable tools that are not sound. Which philosophy should a team betting real money actually trust?&lt;/p&gt;
&lt;h2&gt;7. Four Philosophies, No Winner&lt;/h2&gt;
&lt;p&gt;This is where the zero-knowledge security community actually argues. Not over which tool is best -- over which &lt;em&gt;kind&lt;/em&gt; of tool deserves your trust when funds are on the line. Four camps coexist, and the honest answer is that none of them wins outright.&lt;/p&gt;
&lt;p&gt;The first axis is soundness versus scale, which we have already met. Sound formal tools like Picus prove output uniqueness or hand you a two-witness exploit, but only on gadgets; scalable analyzers like Circomspect and ZKAP run across whole projects but can only flag patterns they already know.&lt;/p&gt;
&lt;p&gt;The second axis is prevention versus detection: strong-Fiat-Shamir libraries and constraint-safer languages try to make a class of bug &lt;em&gt;unwritable&lt;/em&gt;, while linters and verifiers hunt bugs in artifacts that already exist.&lt;/p&gt;
&lt;p&gt;The third axis is human versus automated versus economic: an expert audit reasons about intent, a tool reasons about rules, and a bug bounty pays outsiders to find what both missed.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Sound?&lt;/th&gt;
&lt;th&gt;Scales?&lt;/th&gt;
&lt;th&gt;Prevents or detects&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;th&gt;Misses&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Sound uniqueness verification&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Detects&lt;/td&gt;
&lt;td&gt;Under-constrained circuits, with a counterexample&lt;/td&gt;
&lt;td&gt;Anything past its timeout; non-circuit layers&lt;/td&gt;
&lt;td&gt;Picus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static analysis / linting&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Detects&lt;/td&gt;
&lt;td&gt;Known-bad patterns across many circuits&lt;/td&gt;
&lt;td&gt;Novel absences it has no rule for&lt;/td&gt;
&lt;td&gt;Circomspect, ZKAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong Fiat-Shamir by construction&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Prevents&lt;/td&gt;
&lt;td&gt;The avoidable weak-transcript class&lt;/td&gt;
&lt;td&gt;Circuit and verifier bugs&lt;/td&gt;
&lt;td&gt;Strong-FS libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expert audit&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;td&gt;Detects&lt;/td&gt;
&lt;td&gt;What a skilled reviewer reasons through&lt;/td&gt;
&lt;td&gt;What the reviewer never thinks to check&lt;/td&gt;
&lt;td&gt;Veridise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug bounty&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Detects after ship&lt;/td&gt;
&lt;td&gt;Bugs a motivated outsider finds&lt;/td&gt;
&lt;td&gt;Everything, until value is already at risk&lt;/td&gt;
&lt;td&gt;Solana, BNB disclosures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the table as a set of holes, not a ranking. The sound tools miss everything past their timeout and every layer that is not the circuit. The linters miss the novel absence they have no rule for. Prevention closes only the class it was designed for. Audits miss what the auditor does not think to check. And a bounty, by definition, reports a bug that already shipped.&lt;/p&gt;
&lt;p&gt;The Veil Cash-to-FOOMCASH sequence makes the sharpest case against trusting any single net. The first drain was disclosed with a full technical write-up; six days later the same broken pattern was aimed at a larger target, so a published post-mortem had become an attack -- and rescue -- blueprint inside a week [@zksecurity-groth16, @rekt-unfinished].&lt;/p&gt;
&lt;p&gt;When a bug class&apos;s signature is &lt;em&gt;a missing step with a valid-looking default&lt;/em&gt;, it is trivially easy to reintroduce and just as easy for a copycat to find once the first case is public. Detection after the fact cannot keep pace with that; only prevention -- a Phase-2 check on every deployed key -- gets ahead of it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Both the Solana and BNB reports arrived through disclosure -- which is to say, after the code was live [@solana-june-2025]. The 2026 Groth16 drains are the sharper lesson: white-hats returned most or all of the money, but only &lt;em&gt;after&lt;/em&gt; the broken verifier had already moved it on-chain [@rekt-unfinished]. A bounty is the last line, and a necessary one, but a bug caught by a bounty is a bug that was already exposed. Treat it as a safety net under the other defenses, never as a substitute for them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because the bug is an absence, and absences spread across three layers, no single method can promise to catch them. So the working consensus is not a winner but a discipline: &lt;strong&gt;defense-in-depth, one net per bug type.&lt;/strong&gt; Lint every commit, prove the gadgets, prevent the avoidable class by construction, audit before mainnet, and keep a bounty live after. Each net has holes; the bet is that the holes do not line up.&lt;/p&gt;
&lt;p&gt;Defense-in-depth buys coverage, not certainty. Push on &lt;em&gt;why&lt;/em&gt; no single tool closes the gap, and you hit something deeper than engineering effort -- a wall.&lt;/p&gt;
&lt;h2&gt;8. Why You Cannot Test It Away&lt;/h2&gt;
&lt;p&gt;Suppose every team ran every tool on this list. The bug class still would not disappear -- and the reasons are theorems, not to-do items.&lt;/p&gt;
&lt;p&gt;The first ceiling is the one we met in Section 3, now stated in full generality. A missing constraint is an &lt;em&gt;absence&lt;/em&gt;, not a behavior you can enumerate. Testing samples witnesses and checks that valid ones produce valid proofs; but the illegitimate witnesses live in exactly the region the missing equation was supposed to forbid, so no finite suite of positive tests ever reaches them. You cannot sample your way to a constraint that is not there.&lt;/p&gt;
&lt;p&gt;The second ceiling is the sound-versus-scalable frontier, and it is structural rather than temporary. Detecting under-constraint means searching for a second, distinct satisfying witness -- an NP-hard search, since R1CS satisfiability is NP-complete -- so a sound checker cannot also run efficiently on every input, and deciding uniqueness outright is harder still [@picus-2023-512, @papadimitriou-yannakakis-1984].&lt;/p&gt;
&lt;p&gt;Production circuits are enormous; sound tools resolve gadgets and time out on the rest, which is why roughly a third of even a modest 163-circuit benchmark went unresolved [@picus-2023-512]. Verify gadgets, not whole zkEVMs -- not because the tools are immature, but because the problem is hard in the complexity-theoretic sense.&lt;/p&gt;
&lt;p&gt;The third ceiling sits beneath even a perfectly constrained circuit. The Fiat-Shamir transform has no &lt;em&gt;general, unconditional&lt;/em&gt; standard-model soundness guarantee for the succinct &lt;em&gt;arguments&lt;/em&gt; that SNARKs compress: Goldwasser and Tauman Kalai showed in 2003 that the methodology can be &lt;em&gt;uninstantiable&lt;/em&gt; there, with no guarantee that any concrete hash makes it sound [@gk-2003-034].&lt;/p&gt;
&lt;p&gt;The scope of that claim is worth stating exactly, because it is easy to overstate. For a broad class of public-coin interactive &lt;em&gt;proofs&lt;/em&gt; -- the trapdoor Sigma-protocols -- Fiat-Shamir &lt;em&gt;is&lt;/em&gt; provably sound in the standard model, through correlation-intractable hash families built from the plain Learning-With-Errors assumption [@clw-2018-1248, @peikert-shiehian-2019-158]. But those results instantiate a concrete hash inside a common reference string and cover statistically sound &lt;em&gt;proofs&lt;/em&gt;; they do not reach the succinct &lt;em&gt;arguments&lt;/em&gt;, compiled under the random-oracle heuristic, that shipping SNARKs use -- which sharpens the point rather than softening it.&lt;/p&gt;
&lt;p&gt;And the risk is not academic: in 2025, Khovratovich, Rothblum, and Soukhanov exhibited a &lt;em&gt;practical&lt;/em&gt; Fiat-Shamir break on a widely studied GKR-based argument, holding &quot;for every choice of FS hash function&quot; [@khovratovich-2025-118]. Strong Fiat-Shamir removes the &lt;em&gt;avoidable&lt;/em&gt; class -- forgetting to hash a public value -- but not the heuristic risk underneath it.&lt;/p&gt;
&lt;p&gt;A fourth, quieter limit runs in the same spirit. In the plain model, with no common reference string or random oracle, non-interactive zero-knowledge exists only for languages a verifier could already decide on its own (Goldreich and Oren, 1994), so a hard-language SNARK can &lt;em&gt;relocate&lt;/em&gt; its trusted setup but never &lt;em&gt;remove&lt;/em&gt; it [@goldreich-oren-1994]. The 2026 &lt;code&gt;delta == gamma&lt;/code&gt; drains are that theorem&apos;s practical mirror: a setup cannot be wished away, only executed correctly -- and a skipped Phase-2 step is not an abstraction but a live money bug [@zksecurity-groth16].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The ceiling is a set of theorems, not a backlog. You cannot test an absence away, because a test only exercises what is present. No tool is both sound and scalable, because detecting under-constraint inherits NP-hardness. Strong Fiat-Shamir removes the avoidable mistake without removing the heuristic beneath it. And the trusted setup can be moved but not eliminated. Better tools will help. They will not make the bug class vanish.&lt;/p&gt;
&lt;/blockquote&gt;

The full derivations belong to the structural sibling, [*A Valid Proof of a False Thing*](/blog/a-valid-proof-of-a-false-thing-how-zero-knowledge-proofs-act/): why Fiat-Shamir is uninstantiable in the standard model for the arguments SNARKs use [@gk-2003-034], how a 2025 result forges proofs on a GKR-based argument for every choice of hash function [@khovratovich-2025-118], and why the plain model forces a setup at all [@goldreich-oren-1994]. Two caveats travel with them. Strong Fiat-Shamir closes the mistake of forgetting a public value, not the heuristic risk itself. And the hardness floor is unbroken *so far*, not unbreakable: a STARK&apos;s post-quantum security is a conjecture, not a proof [@starks-2018-046], and a large enough quantum computer running Shor&apos;s algorithm is the one clocked risk to the pairing-based floor.
&lt;p&gt;That last caveat is the article&apos;s other standing hedge. &quot;The crypto never broke&quot; is a statement about the record to date, not a permanence guarantee. The floor has held in every disclosed case. It has not been proven unbreakable, and one class of it -- the algebraic, pairing-based SNARKs -- has a known expiry condition if &lt;a href=&quot;https://paragmali.com/blog/how-q-day-breaks-everything-shors-algorithm-and-the-simultan/&quot; rel=&quot;noopener&quot;&gt;large-scale quantum computing&lt;/a&gt; arrives.&lt;/p&gt;
&lt;p&gt;If the bug class cannot be eliminated, the work is never &quot;done.&quot; It is &lt;em&gt;managed&lt;/em&gt;. So where is that management still failing?&lt;/p&gt;
&lt;h2&gt;9. The Seams Still Open&lt;/h2&gt;
&lt;p&gt;The frontier is a list of gaps everyone in the field can name.&lt;/p&gt;
&lt;p&gt;The first is &lt;strong&gt;scaling sound verification&lt;/strong&gt;. The unresolved tail of the uniqueness checkers -- the roughly one-third of circuits that time out -- is not random. It is exactly the large, non-linear region where production zkEVMs live [@picus-2023-512]. Closing that tail would move sound verification from gadgets to the systems that actually hold billions, and no one yet knows how.&lt;/p&gt;
&lt;p&gt;The second is &lt;strong&gt;portability across representations&lt;/strong&gt;, and it stopped being hypothetical in 2026. Most sound tooling grew up on Circom and R1CS, but the industry has moved to Plonkish arithmetization, AIR, and full zkVMs. When OtterSec broke six of those zkVMs through unbound Fiat-Shamir transcripts, none of the deployed uniqueness or anti-pattern tools would have caught it: they hunt a &lt;em&gt;missing algebraic constraint over circuit signals&lt;/em&gt;, while this was a &lt;em&gt;transcript-ordering&lt;/em&gt; defect one layer up, in the verifier&apos;s absorb-then-challenge sequence [@osec-zkvms].&lt;/p&gt;
&lt;p&gt;The construction-side answer is emerging -- a 2026 framework shows how to compose a special-sound polynomial protocol with a suitably binding commitment so that applying Fiat-Shamir yields a &lt;em&gt;tightly&lt;/em&gt; knowledge-sound SNARK [@eprint-2026-326]. But the detection-side analogue -- a sound automated checker for transcript binding in arbitrary zkVM verifier code, the Picus of the Fiat-Shamir layer -- does not yet exist, and testing cannot stand in for it: honest unit tests run the honest prover, and only manual analysis catches the omission [@osec-zkvms].&lt;/p&gt;
&lt;p&gt;The third is &lt;strong&gt;standardizing transcripts and disclosure&lt;/strong&gt;. The Frozen Heart class exists partly because there was no agreed, hard-to-misuse convention for what a Fiat-Shamir transcript must bind. Strong-FS libraries and transcript-proof fusion help, but a genuinely standard transcript format -- and a shared norm for how ZK bugs are disclosed and dated -- is still forming.&lt;/p&gt;
&lt;p&gt;Running parallel to these is a setup-side gap the 2026 drains exposed. Catching a mis-finalized trusted setup -- a verification key whose &lt;code&gt;delta&lt;/code&gt; still equals &lt;code&gt;gamma&lt;/code&gt; -- is trivial &lt;em&gt;if you know to look&lt;/em&gt;, yet there is no standard, enforced pre-deployment gate that a shipped key actually completed its ceremony [@zksecurity-groth16]. That is a tooling-and-process gap, not a theoretical one, which is what makes it both fixable and, so far, unfixed.&lt;/p&gt;
&lt;p&gt;The fourth is the hardest to see, because it is about the evidence itself.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every prevalence claim in this article -- including &quot;under-constrained circuits are the most common bug&quot; -- is drawn from the bugs that were &lt;em&gt;found&lt;/em&gt; [@chaliasos-sok-arxiv]. The corpus is survivorship-biased by construction. It cannot count the bugs still sitting undisclosed in shipping code, and it cannot tell us whether the clean zero-knowledge record reflects genuinely sound systems or merely undiscovered ones. Read the whole article with that bound in mind.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Measuring &lt;em&gt;true&lt;/em&gt; prevalence -- not just disclosed prevalence -- would require watching for bugs that were never reported, which is close to a contradiction. It may be the one open problem here that tooling alone cannot solve.&lt;/p&gt;
&lt;p&gt;Underneath all four is a maturity gap. Circuits ship on product timelines; verification tools mature on research timelines. As long as the first clock runs faster than the second -- and it now ships whole zkVMs while the tooling is still learning to read them -- the record will keep adding near-misses, and the industry will keep hoping they stay near-misses.&lt;/p&gt;
&lt;p&gt;None of that helps the engineer shipping a circuit this quarter. So here is what you can do today.&lt;/p&gt;
&lt;h2&gt;10. A Reviewer&apos;s Checklist&lt;/h2&gt;
&lt;p&gt;Everything above collapses into a handful of questions you can ask of any zero-knowledge system before it holds value. Ask them layer by layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Circuit.&lt;/strong&gt; Is &lt;em&gt;every&lt;/em&gt; signal constrained, not merely assigned -- is there any &lt;code&gt;&amp;lt;--&lt;/code&gt; or &lt;code&gt;=&lt;/code&gt; without a matching &lt;code&gt;&amp;lt;==&lt;/code&gt;? Is there a range or bit-length check on every input that needs one, before any gadget that assumes it? That single question would have caught Dark Forest, whose fix was exactly a pair of &lt;code&gt;Num2Bits&lt;/code&gt; checks added ahead of a comparator [@df-fix-commit]. Is every uniqueness-critical value -- a nullifier above all -- constrained to a fixed length so it cannot take two forms [@aztec-disclosure]?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verifier.&lt;/strong&gt; Is the on-chain verifier itself the &lt;em&gt;audited&lt;/em&gt; artifact, and is it correct at the edges -- point-at-infinity, zero encodings, on-curve membership? Aztec&apos;s circuit was not the problem; its C++ verifier accepting a zero was [@aztec-0bug].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusted setup.&lt;/strong&gt; If the verifier is a Groth16-style SNARK, did its trusted setup actually finish -- is the verification key&apos;s &lt;code&gt;delta&lt;/code&gt; independent of &lt;code&gt;gamma&lt;/code&gt;, rather than the placeholder generator the two share before Phase 2? A one-line comparison of those two points would have caught Veil Cash and FOOMCASH before either held value [@zksecurity-groth16].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transcript.&lt;/strong&gt; Does the Fiat-Shamir transcript bind the domain separator, the statement, and &lt;em&gt;every&lt;/em&gt; public input, before any challenge is drawn? This is the one-line question behind Frozen Heart, behind Solana&apos;s 2025 near-miss, and behind the 2026 zkVM breaks [@solana-june-2025, @osec-zkvms].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Settlement and integration.&lt;/strong&gt; Does the on-chain settlement loop cover &lt;em&gt;every&lt;/em&gt; public-input slot the circuit committed to, and is a proof &lt;em&gt;actually verified&lt;/em&gt; on every path that moves value? Aztec Connect&apos;s L1 loop read only the slots up to an attacker-controlled count while the proof committed all 32, and Hinkal was reported to have accepted a deposit with no proof at all [@slowmist-aztec, @dextools-hinkal].&lt;/p&gt;
&lt;p&gt;{`
// A toy stand-in for a transcript hash: just concatenate the bound parts.
function challenge(parts) {
  return parts.join(&apos;|&apos;);
}&lt;/p&gt;
&lt;p&gt;const commitment = &apos;C&apos;;
const statementA = &apos;honest public statement&apos;;
const statementB = &apos;false statement the prover prefers&apos;;&lt;/p&gt;
&lt;p&gt;// WEAK Fiat-Shamir: bind only the commitment. The statement is NOT bound.
const weakA = challenge([commitment]);
const weakB = challenge([commitment]);
console.log(&apos;weak:   same challenge for two statements? &apos; + (weakA === weakB));
// true -&amp;gt; the prover can swap the statement without moving the challenge.&lt;/p&gt;
&lt;p&gt;// STRONG Fiat-Shamir: bind the statement and every public input too.
const strongA = challenge([commitment, statementA]);
const strongB = challenge([commitment, statementB]);
console.log(&apos;strong: same challenge for two statements? &apos; + (strongA === strongB));
// false -&amp;gt; swapping the statement changes the challenge, blocking the forge.
`}&lt;/p&gt;
&lt;p&gt;{`
// A Groth16 key that skipped Phase 2 ships with delta == gamma, both still
// the G2 generator placeholder. Detecting it needs no pairing algebra -- just
// a point-equality check: are the two verification-key points identical?&lt;/p&gt;
&lt;p&gt;const G2_GENERATOR = [[10857046, 11559732], [8495653, 4082367]];&lt;/p&gt;
&lt;p&gt;// A key that finished Phase 2: delta was randomized, so it differs from gamma.
const goodKey = {
  vk_gamma_2: [[10857046, 11559732], [8495653, 4082367]],
  vk_delta_2: [[19824234, 55049283], [77712121, 30310670]],
};&lt;/p&gt;
&lt;p&gt;// A key that skipped Phase 2: delta is still the generator, equal to gamma.
const brokenKey = {
  vk_gamma_2: G2_GENERATOR,
  vk_delta_2: G2_GENERATOR,
};&lt;/p&gt;
&lt;p&gt;function phase2Skipped(key) {
  return JSON.stringify(key.vk_gamma_2) === JSON.stringify(key.vk_delta_2);
}&lt;/p&gt;
&lt;p&gt;console.log(&apos;good key   -- Phase 2 skipped? &apos; + phase2Skipped(goodKey));   // false
console.log(&apos;broken key -- Phase 2 skipped? &apos; + phase2Skipped(brokenKey)); // true
console.log(&apos;Equal gamma and delta means the verifier accepts forged proofs.&apos;);
`}&lt;/p&gt;
&lt;p&gt;Then map the tools onto the layers. The point is not to run everything, but to run the right net at the right layer.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug class&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Detect with&lt;/th&gt;
&lt;th&gt;Prevent with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Assigned but not constrained&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Circomspect, ZKAP, Picus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;==&lt;/code&gt; discipline, uniqueness checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing range / bit-length check&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Picus, ZKAP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Num2Bits&lt;/code&gt; on every bounded input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nondeterministic nullifier&lt;/td&gt;
&lt;td&gt;Circuit&lt;/td&gt;
&lt;td&gt;Picus (uniqueness)&lt;/td&gt;
&lt;td&gt;Constrain the tag to a fixed length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier edge case (zero, infinity)&lt;/td&gt;
&lt;td&gt;Verifier&lt;/td&gt;
&lt;td&gt;Audit, differential testing&lt;/td&gt;
&lt;td&gt;Encoding and on-curve checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak Fiat-Shamir&lt;/td&gt;
&lt;td&gt;Transcript&lt;/td&gt;
&lt;td&gt;Audit, transcript review&lt;/td&gt;
&lt;td&gt;Strong-FS library, transcript-proof fusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skipped trusted-setup Phase 2 (&lt;code&gt;delta == gamma&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Verifier / setup&lt;/td&gt;
&lt;td&gt;Compare &lt;code&gt;vk_delta_2&lt;/code&gt; and &lt;code&gt;vk_gamma_2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;zkey contribute&lt;/code&gt;, confirm &lt;code&gt;delta != gamma&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unchecked settlement slot / proofless path&lt;/td&gt;
&lt;td&gt;Settlement / integration&lt;/td&gt;
&lt;td&gt;Audit, differential testing&lt;/td&gt;
&lt;td&gt;Settle every committed slot, verify a proof on every value path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A clean Circomspect or ZKAP run means no known-bad pattern was found -- not that the circuit is sound [@circomspect]. Only a sound uniqueness checker, and only within its timeout, proves absence [@picus-github]. Never let a passing lint stage stand in for a proof that a constraint is actually present.&lt;/p&gt;
&lt;/blockquote&gt;

Given a `RangeProof` that calls `LessThan(64)` on an input, ask: where is that input constrained to 64 bits? If the only bit-length logic in the circuit lives *inside* `LessThan`, there is none -- the gadget assumes it. The fix is a `Num2Bits(64)` on each input before the comparison, which is exactly the commit Dark Forest shipped [@df-fix-commit]. The verifier-side analogue is just as mechanical: given a deployed Groth16 key, print `vk_gamma_2` and `vk_delta_2` and compare them. If they are the same point, Phase 2 was skipped and the verifier will accept forged proofs -- the Veil Cash and FOOMCASH bug in a single check [@zksecurity-groth16].
&lt;p&gt;One last item belongs on any honest checklist: knowing when zero-knowledge is the wrong tool. A proof system adds a large, subtle artifact -- three layers, each independently fallible -- on top of whatever it protects. If a cheaper mechanism gives the same guarantee, the safest circuit is the one you never had to write.&lt;/p&gt;
&lt;p&gt;A checklist is only as good as the misconceptions it clears. Here are the ones this topic breeds.&lt;/p&gt;
&lt;h2&gt;11. Questions the Record Answers&lt;/h2&gt;
&lt;p&gt;The topic breeds a handful of durable misconceptions. Here are the ones worth clearing.&lt;/p&gt;


Not on its own. Soundness is relative to the circuit&apos;s specification. A valid proof means *some* witness satisfied the constraints that were actually written. If a constraint is missing, an illegitimate witness qualifies, and the proof certifies something the designer never intended. The proof is honest about the constraints; the constraints may not be honest about the world.


No. Every publicly disclosed case is an artifact-layer soundness bug -- in a circuit, a verifier, or a transcript. The discrete-log, pairing, and hash assumptions underneath never appear in the record, and the empirical corpus of 141 SNARK vulnerabilities is overwhelmingly made of these soundness bugs [@chaliasos-sok-arxiv].


Yes, more than once -- but the honest answer needs tiers. Every confirmed on-chain money loss traces to a verifier, settlement, trusted-setup, or integration mismatch rather than a pure in-circuit under-constraint [@samczsun-bnb, @slowmist-aztec, @darknavy-veil], while the pure in-circuit cases -- Zcash, Dark Forest, Solana&apos;s ElGamal program -- were disclosed-and-patched near-misses; the full incident-by-incident accounting, with figures and disposition tags, is the ledger above.


No. It was a proactive, coordinated disclosure across Girault, Bulletproofs, and PlonK implementations; affected parties remediated before publication [@tob-frozenheart-p1]. The same weak-Fiat-Shamir omission has now recurred across four eras -- Helios in 2012, Frozen Heart in 2022, Solana&apos;s ZK ElGamal program in 2025, and six production zkVMs disclosed by OtterSec in 2026 -- and every instance was a disclosed near-miss with no known exploitation [@bpw-2016-771, @solana-june-2025, @osec-zkvms]. &quot;In the wild&quot; here means the weak transcript was present in shipping code, not that anyone forged a proof with it.


Under-constrained circuits. In the SoK 2024 study of 141 real-world SNARK vulnerabilities, they were the single most prevalent category, in a corpus dominated by soundness failures rather than privacy or completeness ones [@chaliasos-sok-arxiv].


Only partly. Sound tools such as Picus prove output uniqueness or hand you a two-witness exploit, but they do not scale to production-size circuits [@picus-2023-512]. Scalable linters such as Circomspect and ZKAP run across whole projects but are unsound -- a clean scan is not a proof [@circomspect, @zkap-2023-190]. And none of them checks whether your *trusted setup* was actually finalized, the gap the 2026 Groth16 drains walked through [@zksecurity-groth16]. No deployed tool covers every layer, which is why the discipline is defense-in-depth.


No. The structural sibling *derives* the mechanisms -- why a redundant CRS, a weak transcript, or an uninstantiable heuristic can break a proof. This article is the *documented record* of those mechanisms shipping, breaking, and being caught, tagged incident by incident.

&lt;p&gt;Step back and the ledger reads the same from every angle. In each publicly disclosed failure of a production zero-knowledge system, the cryptographic floor held: no curve solved, no hash reversed, no pairing broken. The human-built artifact above it leaked -- a circuit missing a constraint, a verifier mishandling a zero, a transcript that hashed too little, or integration code that forgot to check the proof at all.&lt;/p&gt;
&lt;p&gt;And where that artifact guarded a rollup, a pool, or a bridge, the leak was denominated in money: minted or drained in the near-misses, moved and then returned in the 2026 Groth16 drains, and stolen outright in the confirmed cases, with the largest still in the adjacent BNB Merkle verifier.No confirmed theft in this record is a &lt;em&gt;pure&lt;/em&gt; zk-SNARK circuit break: BNB was a Merkle-proof verifier bug; Veil Cash and FOOMCASH were Groth16 verifier-key bugs; Hinkal was reported as a proofless-deposit integration bug; and Aztec Connect paired a circuit-side gap-slot problem with a settlement-layer flaw. That is the distinction this whole article has kept, so that &quot;the bugs that minted money&quot; never quietly becomes &quot;zero-knowledge cryptography was broken.&quot;&lt;/p&gt;

The crypto held; the code didn&apos;t -- in every disclosed case, and so far.
&lt;p&gt;That refrain carries its own hedge. &quot;So far&quot; is doing real work: the record is short, the corpus counts only the bugs that were found, and the floor is unbroken rather than unbreakable -- a STARK&apos;s post-quantum security is still a conjecture, and Shor&apos;s algorithm remains the one clocked risk [@starks-2018-046].&lt;/p&gt;
&lt;p&gt;The honest claim is not that zero-knowledge cannot break at the math. It is that, to date, it never has -- and every time it broke in real life, the fault was a line of code a human forgot to write. To see &lt;em&gt;why&lt;/em&gt; those mechanisms can fail, read the structural sibling, &lt;a href=&quot;https://paragmali.com/blog/a-valid-proof-of-a-false-thing-how-zero-knowledge-proofs-act/&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;A Valid Proof of a False Thing&lt;/em&gt;&lt;/a&gt;; to see how the proof systems are built in the first place, read &lt;a href=&quot;https://paragmali.com/blog/never-decrypted-proving-you-ran-the-computation-without-reve/&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;Never Decrypted: Proving You Ran the Computation&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;how-zero-knowledge-broke-in-real-life&quot; keyTerms={[
  { term: &quot;Soundness&quot;, definition: &quot;The guarantee that no one can produce a convincing proof of a false statement; the property every incident in this article attacks.&quot; },
  { term: &quot;Under-constrained circuit&quot;, definition: &quot;A circuit whose equations admit a witness the designer never intended, because the excluding constraint was never written; the most prevalent class in the disclosed corpus.&quot; },
  { term: &quot;Range check&quot;, definition: &quot;A constraint forcing a value to fit in n bits; omitting it lets an oversized field element pass as in-range, as in the Dark Forest bug.&quot; },
  { term: &quot;Verifier&quot;, definition: &quot;The code that checks a proof, usually an on-chain contract; a separate artifact from the circuit that can be independently wrong, as in the Aztec 0-bug.&quot; },
  { term: &quot;Fiat-Shamir transform&quot;, definition: &quot;Replacing a public-coin verifier&apos;s random challenge with a hash of the transcript; sound only when the hash binds the statement and all public inputs.&quot; },
  { term: &quot;Weak Fiat-Shamir&quot;, definition: &quot;A transcript that hashes too little, letting a prover steer the challenge; the Frozen Heart, Solana, and zkVM root cause.&quot; },
  { term: &quot;Nullifier&quot;, definition: &quot;A unique tag published when a note is spent to prevent double-spends; must be deterministic, or one note yields many tags.&quot; }
]} questions={[
  { q: &quot;Why can a valid proof still certify a false statement?&quot;, a: &quot;Because soundness is relative to the constraints actually written. If a constraint is missing, an illegitimate witness satisfies the system and the proof certifies it.&quot; },
  { q: &quot;Why can&apos;t unit tests catch a missing constraint?&quot;, a: &quot;The bug is an absence. Positive tests all pass, because the illegitimate witnesses live in exactly the space the missing equation was meant to forbid.&quot; },
  { q: &quot;What distinguishes the BNB theft from the zero-knowledge near-misses?&quot;, a: &quot;BNB was a general IAVL Merkle proof-verifier bug that skipped a branch, not a zk-SNARK circuit under-constraint; it is the adjacent archetype, and the largest confirmed theft.&quot; },
  { q: &quot;Why is no single tool enough?&quot;, a: &quot;Sound tools do not scale and scalable tools are not sound, so the working answer is defense-in-depth: one net per bug type across all three layers.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>zero-knowledge-proofs</category><category>zk-snarks</category><category>under-constrained-circuits</category><category>soundness</category><category>fiat-shamir</category><category>circuit-security</category><category>zk-security</category><category>blockchain-security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>A Valid Proof of a False Thing: How Zero-Knowledge Proofs Actually Break</title><link>https://paragmali.com/blog/a-valid-proof-of-a-false-thing-how-zero-knowledge-proofs-act/</link><guid isPermaLink="true">https://paragmali.com/blog/a-valid-proof-of-a-false-thing-how-zero-knowledge-proofs-act/</guid><description>Zero-knowledge proofs rarely break at the math. They break in the circuit, the trusted setup, and the Fiat-Shamir transcript -- here is exactly how, and why.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>
Production zero-knowledge proofs almost never break at the cryptographic hardness. Across the entire documented incident record, the discrete-log, pairing, and hash assumptions held. The breaks live one layer up, in the artifact humans compile: **under-constrained circuits** (a missing constraint lets a prover forge a valid proof of a false statement -- the single most prevalent bug class), **trusted-setup subversion** (whoever keeps the setup trapdoor can forge any proof), and the **Frozen Heart** class (a weak Fiat-Shamir transcript that hashes too little). Name the layer -- circuit, setup string, or transcript -- and you have named the break. Even a STARK&apos;s transparent, hash-based construction only sheds one seam while inheriting the others, and &quot;post-quantum secure&quot; remains a conjecture, not a proof.
&lt;h2&gt;1. A Valid Proof of a False Thing&lt;/h2&gt;
&lt;p&gt;A verifier checks a zero-knowledge proof and returns a single bit: &lt;code&gt;true&lt;/code&gt;. Every pairing balances. Every hash matches. The discrete-logarithm assumption the whole scheme rests on was never in doubt. And yet the statement the proof certifies is false, the money it authorized has already left the vault, and no one broke any cryptography to make it happen.&lt;/p&gt;
&lt;p&gt;That sentence should read like a contradiction. A proof is supposed to be a guarantee, and &quot;verified&quot; is supposed to mean &quot;true.&quot; Dissolving the paradox is the entire project of this article, and the resolution is deceptively small: a proof is valid relative to &lt;em&gt;the constraints someone wrote down&lt;/em&gt;, not relative to the truth they meant to capture.&lt;/p&gt;

A protocol that convinces a verifier a statement is true while revealing nothing beyond its truth. The word &quot;argument&quot; (as opposed to &quot;proof&quot;) signals *computational* soundness: the guarantee holds against a prover with bounded computing power, which is the setting every deployed SNARK actually lives in.
&lt;p&gt;If the constraints never force the prover to be honest about some value, then a proof that verifies is not a lie about the mathematics. It is an honest proof of the wrong statement. The verifier did its job perfectly. The specification it was checking simply was not the specification the designer had in mind.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A verifying proof means &quot;the prover found some witness the circuit accepts,&quot; not &quot;the statement is true.&quot; Those two readings coincide only when the circuit was written to force them together. Every break in this article is a place where they came apart.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To see how that gap opens, picture a zero-knowledge system as two stacked layers.&lt;/p&gt;

flowchart TD
    subgraph Artifact[&quot;Human-built artifact -- where every break lives&quot;]
      C[Arithmetized circuit]
      S[Trusted-setup string]
      T[Fiat-Shamir transcript]
    end
    Artifact --&amp;gt; Floor
    subgraph Floor[&quot;Hardness floor -- unbroken so far&quot;]
      D[Discrete logarithm]
      P[Elliptic-curve pairings]
      H[Collision-resistant hash]
    end
&lt;p&gt;At the bottom sits a &lt;strong&gt;hardness floor&lt;/strong&gt;: a small set of problems believed intractable, such as computing discrete logarithms, solving pairing relations, or finding hash collisions. At the top sits a &lt;strong&gt;human-built artifact&lt;/strong&gt;: an arithmetic circuit that encodes the statement, a setup string that parameterizes the system, and a Fiat-Shamir transcript that turns an interactive exchange into a single non-interactive proof. Cryptographers design the floor over decades and attack it relentlessly. Engineers assemble the artifact per project, often under deadline.&lt;/p&gt;
&lt;p&gt;Here is the thesis this article defends, drawn from the documented vulnerability record rather than from any theorem: &lt;strong&gt;production zero-knowledge proofs almost never fail at the cryptographic hardness. They fail at soundness in the layer humans build&lt;/strong&gt; [@sok24]. The floor has held in every deployed break on record. The three seams above it -- the circuit, the setup string, and the transcript -- are where the leaks happen, and this article anatomizes each one.&lt;/p&gt;
&lt;p&gt;One boundary before we start. This is a structural teardown: we are after the ways the artifact itself is mis-designed. The separate world of implementation faults -- timing side channels, faulty randomness, nonce leakage -- is real and dangerous, but it belongs to a sibling article, &quot;How Zero-Knowledge Broke in Real Life.&quot; Here, every bug we examine would still be a bug if the code ran on perfect hardware with a perfect random-number generator.&lt;/p&gt;
&lt;p&gt;To understand why the floor and the seams are even &lt;em&gt;separable&lt;/em&gt; -- why you can lose soundness while discrete logarithm stands untouched -- we have to go back to 1985, to the paper that first pulled a proof apart into three independent pieces.&lt;/p&gt;
&lt;h2&gt;2. Where Soundness Was Born&lt;/h2&gt;
&lt;p&gt;Before 1985, a proof was a static object. You held a witness -- a satisfying assignment, a factorization, a certificate -- and you handed it over. The verifier re-checked it and was convinced. Soundness in that world was absolute and &lt;em&gt;fused&lt;/em&gt; to the statement: a false claim simply has no accepting witness, so cheating is impossible by definition.&lt;/p&gt;
&lt;p&gt;But that same fusion meant there was no vocabulary for a cheating prover. You could not ask &lt;em&gt;how much&lt;/em&gt; a liar might get away with, or measure it apart from the underlying hard problem. There was no &quot;soundness error&quot; to reason about, because soundness was all-or-nothing and welded to truth.&lt;/p&gt;
&lt;p&gt;Shafi Goldwasser, Silvio Micali, and Charles Rackoff broke that object into pieces. Their 1985 paper on the knowledge complexity of interactive proof systems replaced the static witness with an interactive protocol: a prover and a &lt;em&gt;randomized&lt;/em&gt; verifier trade messages, the verifier issues fresh unpredictable challenges, and it accepts only probabilistically [@gmr85].&lt;/p&gt;
&lt;p&gt;In doing so they separated a proof into three properties that had never been named apart before -- &lt;strong&gt;completeness&lt;/strong&gt;, &lt;strong&gt;soundness&lt;/strong&gt;, and &lt;strong&gt;zero-knowledge&lt;/strong&gt; -- and, decisively, they made soundness a &lt;em&gt;quantity&lt;/em&gt;. A cheating prover now succeeds with some probability, the soundness error, that you can drive toward zero but analyze on its own terms [@gmr85].&lt;/p&gt;
&lt;p&gt;A &quot;proof&quot; in the strict sense has soundness against a computationally &lt;em&gt;unbounded&lt;/em&gt; cheating prover; an &quot;argument&quot; only against a bounded one. Almost every deployed SNARK is technically an argument. The distinction matters because argument soundness can rest on a hardness assumption, which is exactly the floor this article shows never cracks.&lt;/p&gt;
&lt;p&gt;That separation is the hinge of everything that follows. Because soundness is measured against &lt;em&gt;the statement as encoded&lt;/em&gt;, not against discrete logarithm, a proof can be cryptographically valid and still certify a falsehood -- the encoded statement was wrong, even though the mathematics behind it was pristine. GMR gave the field the language to say that sentence.&lt;/p&gt;
&lt;p&gt;Then came two conveniences, each of which made proofs deployable and each of which opened a seam. In 1986, Amos Fiat and Adi Shamir showed you could remove the live verifier entirely [@fs86].&lt;/p&gt;

A method for making an interactive proof non-interactive by computing the verifier&apos;s random challenge as a hash of the conversation so far, rather than waiting for a human or a live party to supply it. The prover hashes its own commitment (and, done correctly, the statement) to derive the challenge, so anyone can recompute and check it offline.
&lt;p&gt;The Fiat-Shamir transform is the universal glue beneath essentially every non-interactive proof and &lt;a href=&quot;https://paragmali.com/blog/the-discrete-log-held-the-proofs-leaked-a-field-guide-to-com/&quot; rel=&quot;noopener&quot;&gt;Schnorr-style signature&lt;/a&gt; in production -- and, as we will see, the exact step whose mis-implementation becomes the Frozen Heart class thirty-six years later. Two years after Fiat-Shamir, Manuel Blum, Paul Feldman, and Silvio Micali added the other convenience: a shared &lt;strong&gt;common reference string&lt;/strong&gt; that lets a single message be a full zero-knowledge proof inside a security model rather than merely a heuristic [@bfm88]. Every later structured setup -- Groth16&apos;s reference string, the &quot;powers of tau&quot; ceremonies -- is a concrete descendant of that idea.&lt;/p&gt;

flowchart LR
    A[1985 GMR: three properties] --&amp;gt; B[1986 Fiat-Shamir transform]
    B --&amp;gt; C[1988 BFM setup string]
    C --&amp;gt; D[2012 Helios weak Fiat-Shamir]
    D --&amp;gt; E[2016 Groth16 toxic waste]
    E --&amp;gt; F[2019 Zcash redundant CRS]
    F --&amp;gt; G[2022 Frozen Heart]
    G --&amp;gt; H[2025 practical Fiat-Shamir break]
&lt;p&gt;Could you simply delete the setup seam by using no setup at all? In 1994, Oded Goldreich and Yair Oren proved you cannot: non-trivial non-interactive zero-knowledge in the plain model, with no setup whatsoever, collapses to the class BPP -- meaning it buys you nothing beyond ordinary randomized computation [@go94]. A setup of &lt;em&gt;some&lt;/em&gt; kind is mandatory. This theorem is why a careful engineer says a transparent system has &quot;no &lt;em&gt;trusted&lt;/em&gt; setup&quot; and never &quot;no setup.&quot; The distinction is load-bearing, not pedantic, and it will decide how we read STARKs later.&lt;/p&gt;
&lt;p&gt;&quot;Transparent&quot; setup means the setup uses only public randomness that anyone can verify -- a public-coin setup -- so there is no secret trapdoor to leak. Goldreich and Oren [@go94] show you can downgrade a &lt;em&gt;trusted&lt;/em&gt; setup to a public-coin one, but you can never reach &lt;em&gt;no&lt;/em&gt; setup for a non-trivial statement.&lt;/p&gt;
&lt;p&gt;Three properties, three ways to fail. Before we watch each one break in production, we need the grid that tells us which failure we are looking at -- and, more to the point, which one steals money.&lt;/p&gt;
&lt;h2&gt;3. Three Ways to Fail, One That Pays&lt;/h2&gt;
&lt;p&gt;A zero-knowledge system can fail along exactly three axes, because GMR gave it exactly three properties [@gmr85]. Only one of those failures makes an attacker rich. Getting clear on which is which is the analytical spine of everything that follows, so it is worth building the grid deliberately.&lt;/p&gt;

The guarantee that a cheating prover cannot convince an honest verifier of a false statement, except with negligibly small probability -- the soundness error. Soundness is the property an attacker attacks: break it and you can prove things that are not true.

The guarantee that an honest prover holding a valid witness always convinces an honest verifier. A completeness failure rejects legitimate proofs. It denies service; it does not manufacture forgeries.

The guarantee that a proof reveals nothing beyond the truth of the statement, formalized by a simulator that produces a transcript indistinguishable from a real one without ever seeing the witness. A zero-knowledge failure leaks the secret. It does not let anyone prove a falsehood.
&lt;p&gt;Line those three up against their consequences and the asymmetry is stark.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure axis&lt;/th&gt;
&lt;th&gt;What goes wrong&lt;/th&gt;
&lt;th&gt;Consequence&lt;/th&gt;
&lt;th&gt;Who attacks it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;td&gt;Honest prover gets rejected&lt;/td&gt;
&lt;td&gt;Liveness / denial of service&lt;/td&gt;
&lt;td&gt;Usually no one -- it is a self-inflicted bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Soundness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;False statement accepted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Forgery, minting, theft&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A malicious prover&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-knowledge&lt;/td&gt;
&lt;td&gt;Witness is exposed&lt;/td&gt;
&lt;td&gt;Privacy loss, de-anonymization&lt;/td&gt;
&lt;td&gt;A malicious verifier or observer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the table as a threat model. A &lt;strong&gt;completeness&lt;/strong&gt; failure is annoying but not lucrative: your honest users cannot get their valid proofs accepted, so the system grinds rather than bleeds. A &lt;strong&gt;zero-knowledge&lt;/strong&gt; failure is a privacy catastrophe -- the secret the proof was meant to hide leaks out -- but it forges nothing; the attacker learns a witness, they do not mint one.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;soundness&lt;/strong&gt; failure is the money axis. When a malicious prover convinces an honest verifier of something false, and that verifier is a smart contract that releases funds or mints tokens, the false statement becomes a withdrawal. This is why soundness is the axis attackers actually target, and the axis this article spends its time on.&lt;/p&gt;
&lt;p&gt;The empirical record confirms the asymmetry rather than merely asserting it. The 2024 systematization of SNARK vulnerabilities examined 141 disclosed bugs and found that the overwhelming majority compromise soundness -- on the order of 95 of them -- while only a handful, around four, affect completeness [@sok24]. The disclosed corpus is, in effect, a ledger of soundness breaks. And these are not abstract failures.&lt;/p&gt;

&quot;Several million dollars worth of cryptocurrency have been stolen due to underconstrained arithmetic circuits.&quot; -- the Picus/QED2 authors and the IACR announcement of their work [@picus23][@iacr20490]
&lt;p&gt;Notice what that sentence does &lt;em&gt;not&lt;/em&gt; say. It does not say the &lt;a href=&quot;https://paragmali.com/blog/the-log-was-never-the-weak-part-how-discrete-log-cryptograph/&quot; rel=&quot;noopener&quot;&gt;discrete-logarithm assumption&lt;/a&gt; fell, or that someone found a hash collision, or that a pairing was inverted. The theft came from circuits that were &lt;em&gt;under-constrained&lt;/em&gt; -- a soundness failure in the arithmetization layer, the top of the stack, with the hardness floor entirely intact [@picus23]. That is the pattern in miniature, and we are about to see it three times over.&lt;/p&gt;
&lt;p&gt;Every real break in the rest of this article lands on the soundness axis, and each one lives in a &lt;em&gt;different layer&lt;/em&gt; of the artifact.&lt;/p&gt;
&lt;h2&gt;4. The Break Surface, Layer by Layer&lt;/h2&gt;
&lt;p&gt;The artifact humans build has three parts, and each one has failed in production. We will walk them from the top down: the &lt;strong&gt;arithmetization&lt;/strong&gt; layer where a computation becomes equations, the &lt;strong&gt;setup&lt;/strong&gt; layer where the system is parameterized, and the &lt;strong&gt;transform&lt;/strong&gt; layer where an interactive proof is frozen into a non-interactive one. This is not a smooth ascent toward safety. It is the same small set of bugs reappearing as the systems industrialize, generation after generation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The next three subsections each take one layer, explain the mechanism precisely, and end at the failure that motivates the next. Keep one structural fact in view throughout: the seams are &lt;em&gt;orthogonal&lt;/em&gt;. Fixing the circuit does nothing for the setup string; removing the trapdoor does nothing for the transcript. That independence is the observation the whole article turns on, and we will name it explicitly once all three are on the table.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We start with the arithmetization layer, the one that breaks most often and most expensively [@sok24].&lt;/p&gt;
&lt;h3&gt;4a. The Circuit -- Under-Constrained Arithmetization&lt;/h3&gt;
&lt;p&gt;Return to the forged proof from the opening. We can now say exactly what happened to it. Before a statement can be proved, it has to be compiled into a form a proof system understands, and that compilation is done by hand.&lt;/p&gt;

The compilation of a computation into a system of equations over a finite field. The most common target is a rank-1 constraint system, or R1CS: a long list of quadratic constraints, each of the form &quot;one linear combination of the signals, times another, equals a third.&quot; The signals are the inputs, outputs, and intermediate values; &quot;proving the computation&quot; means exhibiting a signal assignment that satisfies every constraint at once.
&lt;p&gt;A circuit, then, is a hand-written compiler artifact: the programmer&apos;s mental model of a computation, flattened into equations. That compilation has a lineage -- the quadratic-span-program and Pinocchio constructions turned a constraint system into a form a pairing can check in constant time, which is what made general-purpose SNARKs practical [@ggpr13][@pinocchio]. And like any hand-written artifact, it can be missing a line. What happens when the human forgets one of the equations?&lt;/p&gt;

A constraint system that admits more than one satisfying assignment for a signal the designer meant to pin down. Because several witnesses satisfy the same constraints, a prover can pick one the specification never intended and still produce a proof the verifier accepts.
&lt;p&gt;That is the whole mechanism. A missing constraint means the system of equations has too much slack: the value the developer assumed was forced is actually free. A malicious prover fills that freedom with an out-of-spec value, satisfies every constraint that &lt;em&gt;is&lt;/em&gt; present, and hands the verifier a proof that checks perfectly.&lt;/p&gt;

flowchart TD
    A[Computation compiled to a constraint system] --&amp;gt; B[One constraint omitted]
    B --&amp;gt; C{&quot;More than one witness fits?&quot;}
    C --&amp;gt;|Yes| D[Prover supplies an out-of-spec value]
    C --&amp;gt;|No| E[Output uniquely determined and safe]
    D --&amp;gt; F[Verifier accepts a valid proof of a false statement]
&lt;p&gt;The canonical toy version is a square. Suppose you want to prove &quot;I know the secret value the designer had in mind, and its square is &lt;code&gt;x&lt;/code&gt;.&quot; The obvious constraint is that the square of the witness equals &lt;code&gt;x&lt;/code&gt;. But that single equation does not pin the witness: in a field, both a root and its negation square to the same value, so two different witnesses satisfy one constraint. Run it and watch both slip through.&lt;/p&gt;
&lt;p&gt;{`
// A toy field-arithmetic &quot;circuit&quot; over the small prime p = 23.
// Intended statement: &quot;I know the specific value y the designer meant,
// and its square is x.&quot; The circuit writes only ONE constraint:
//        x === (y * y) mod p
// It never pins WHICH square root y is -- so the value is under-determined.
const p = 23;
const mod = (a) =&amp;gt; ((a % p) + p) % p;&lt;/p&gt;
&lt;p&gt;function circuitAccepts(x, y) {
  return mod(y * y) === mod(x);   // the single constraint actually enforced
}&lt;/p&gt;
&lt;p&gt;const x = 4;            // the public output the verifier sees
const intended = 2;     // the witness the designer assumed
const forged = p - 2;   // 21: a DIFFERENT witness, same constraint&lt;/p&gt;
&lt;p&gt;console.log(&quot;intended y =&quot;, intended, &quot;accepted:&quot;, circuitAccepts(x, intended));
console.log(&quot;forged   y =&quot;, forged,   &quot;accepted:&quot;, circuitAccepts(x, forged));
// Both print true. A proof that &quot;verifies&quot; has not pinned the value the
// specification intended -- it only certifies that SOME accepted witness exists.
`}&lt;/p&gt;
&lt;p&gt;In a toy square the extra witness is harmless. In a withdrawal circuit that was supposed to enforce &quot;this note has not already been spent&quot; or &quot;this balance is non-negative,&quot; the extra witness is a second spend or a negative amount that verifies as valid -- a mint or a theft, produced without touching the cryptography underneath.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A verifying proof certifies that the prover satisfied the &lt;em&gt;circuit&lt;/em&gt;, not that they satisfied your &lt;em&gt;specification&lt;/em&gt;. If a constraint you assumed is missing, the verifier will accept out-of-spec witnesses all day. &quot;Valid&quot; is a claim about the equations on the page, never about your intentions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the payoff of the first aha moment. The naive reading -- &quot;a verifying proof means the statement is true&quot; -- fails because a verifying proof only means the prover found &lt;em&gt;some&lt;/em&gt; witness the circuit accepts. When the circuit is under-constrained, that witness can be an out-of-spec lie, and the discrete-logarithm assumption is fully intact the entire time.&lt;/p&gt;
&lt;p&gt;The archetypal way this bug enters real code is a one-character slip.&lt;/p&gt;
&lt;p&gt;In Circom, the most widely used circuit language, &lt;code&gt;&amp;lt;--&lt;/code&gt; assigns a value to a signal but adds no constraint, while &lt;code&gt;&amp;lt;==&lt;/code&gt; assigns &lt;em&gt;and&lt;/em&gt; constrains. Writing &lt;code&gt;&amp;lt;--&lt;/code&gt; where you meant &lt;code&gt;&amp;lt;==&lt;/code&gt; leaves the signal free -- the exact shape of an under-constrained bug. Trail of Bits built the Circomspect linter specifically to flag that slip and related unconstrained-signal patterns [@circomspect_gh][@circomspect_blog].&lt;/p&gt;
&lt;p&gt;The field arithmetic hides further traps: values that alias modulo the field prime, and &quot;hint&quot; signals computed outside the circuit that are trusted without an in-circuit check. Each is a different way to leave a signal under-determined, and cataloguing them all is a project of its own. The structural point is singular: every one of these is a missing constraint, and a missing constraint is a soundness bug in the layer you wrote.&lt;/p&gt;
&lt;p&gt;How prevalent is it? The 2024 systematization is blunt: across 141 disclosed SNARK vulnerabilities, under-constrained circuits are the single most prevalent class [@sok24]. This is not an exotic failure that shows up once a decade. It is the modal bug of the entire field, which is why it gets the most tooling -- and why we can catch it if we ask the right question.&lt;/p&gt;

The property you actually want is *determinism*: every output signal should be uniquely determined by the inputs. You can test it mechanically -- ask a solver whether two different witnesses can share the same inputs yet differ on an output. If the answer is yes, the circuit is under-constrained. That is exactly what the Picus/QED2 tool automates [@picus23], and Section 10 turns it into a concrete review step.
&lt;p&gt;A missing constraint is a bug in a layer &lt;em&gt;you&lt;/em&gt; wrote and can, in principle, read. But what if the flaw is baked into a string you were &lt;em&gt;handed&lt;/em&gt; -- one you cannot even inspect, generated in a ritual you had to trust?&lt;/p&gt;
&lt;h3&gt;4b. The Reference String -- Trusted-Setup Subversion and Toxic Waste&lt;/h3&gt;
&lt;p&gt;Some of the most efficient proof systems need a one-time setup that samples secret numbers, uses them to build a public parameter string, and then -- supposedly -- destroys them. The whole security argument rests on that destruction actually happening. What if someone keeps a copy?&lt;/p&gt;

A structured reference string (SRS) is a setup string whose elements carry algebraic structure: they are group encodings of secret scalars sampled once at setup time. Only the encodings are published. The secret scalars themselves are the &quot;toxic waste.&quot; Anyone who retains them holds a trapdoor that can forge a proof of *any* statement, because the trapdoor lets them satisfy the verification equation without a real witness.
&lt;p&gt;This is the setup layer&apos;s first and cleanest failure mode, and Groth16 is its reference example. Jens Groth&apos;s 2016 construction produces the smallest proof among deployed pairing-based SNARKs -- three group elements checked by a single pairing-product equation [@groth16] -- and that compactness is exactly what makes it attractive for gas-sensitive on-chain verification. But its soundness depends on an &lt;em&gt;operational&lt;/em&gt; assumption, not a cryptographic one: it assumes the toxic waste was destroyed [@groth16]. If it was not, the holder forges at will, and the pairing assumption underneath never budges.&lt;/p&gt;

flowchart TD
    A[Setup samples secret scalars] --&amp;gt; B[Publish only their group encodings as the SRS]
    B --&amp;gt; C{&quot;Trapdoor destroyed?&quot;}
    C --&amp;gt;|Yes, via ceremony| D[No one can forge, system is sound]
    C --&amp;gt;|No, someone kept it| E[Holder forges any statement at will]
&lt;p&gt;This is &lt;em&gt;why the ceremony matters&lt;/em&gt;. To avoid trusting a single party to destroy the trapdoor, the setup is run as a multi-party computation: each participant folds in fresh secret randomness and destroys their own share, and the forgery-enabling trapdoor is the product of all shares. It is unrecoverable unless &lt;em&gt;every&lt;/em&gt; participant colludes, which flips the requirement into a reassuring form -- the setup is secure if &lt;em&gt;at least one&lt;/em&gt; of the participants was honest [@eth_kzg_blog].&lt;/p&gt;
&lt;p&gt;These are the &quot;powers of tau&quot; ceremonies, named for the secret scalar tau whose successive powers structure the string. The security property is the &quot;1-of-N honest&quot; assumption: among N contributors, a single honest one who truly destroyed their randomness makes the trapdoor unrecoverable [@eth_kzg_blog].&lt;/p&gt;
&lt;p&gt;The modern scale of this is remarkable. Ethereum&apos;s KZG Summoning Ceremony ran for 208 days and collected 141,416 contributions, and its security rests on needing only a single honest participant among them [@eth_kzg_blog]. That turns an unavoidable trusted setup into a broadly auditable social process. The residual cost is real, though: the guarantee is social, never provable, and for Groth16 the ceremony is &lt;em&gt;per-circuit&lt;/em&gt; -- every new program needs its own [@eth_kzg_blog].&lt;/p&gt;
&lt;p&gt;Now the accuracy-critical part. There is a &lt;em&gt;second&lt;/em&gt;, entirely different setup-layer failure that is routinely conflated with the first, and conflating them gets the physics wrong.&lt;/p&gt;

**Mode 1 -- toxic-waste retention.** Someone keeps the secret scalars a correct setup was supposed to destroy, and uses that trapdoor to forge. The construction is fine; the secret leaked. The ceremony is the defense.&lt;p&gt;&lt;strong&gt;Mode 2 -- a construction soundness bug.&lt;/strong&gt; The setup string itself is built wrong -- it contains elements that should not be there -- so a prover can forge &lt;em&gt;even though no secret was ever retained&lt;/em&gt;. No ceremony helps, because nothing leaked. The construction is the defect.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Mode 2 is what actually happened to Zcash. The SNARK it deployed, BCTV14, carried redundant elements in its reference string that the earlier Pinocchio template had deliberately excluded [@gabizon19][@bctv14]. Ariel Gabizon showed those redundant elements let a prover convert a valid proof of one statement into a valid-looking proof of a &lt;em&gt;different&lt;/em&gt; statement -- undetectable, unlimited counterfeiting, with the hardness assumption completely intact [@gabizon19].&lt;/p&gt;

The flaw let an attacker &quot;transform the proof of one statement into an ostensibly valid proof of a different statement, thereby breaking ... soundness.&quot; -- the official record of CVE-2019-7167 [@cve19]
&lt;p&gt;The timeline is the quietly alarming part, and it hides two windows that are easy to conflate. The Electric Coin Company&apos;s postmortem records that the flaw was discovered on 1 March 2018, silently remediated by the Sapling network upgrade that activated on 28 October 2018, and only disclosed on 5 February 2019 [@ecc19].&lt;/p&gt;
&lt;p&gt;Those two windows are very different lengths. The &lt;em&gt;exploitable&lt;/em&gt; window was the long one: the flaw was present from Zcash&apos;s 2016 launch until the Sapling fix, roughly two years in which the currency was silently, undetectably counterfeitable. The &lt;em&gt;secrecy&lt;/em&gt; window was only the eleven months from private discovery to public disclosure. Across both, no observer could have told from the chain, because a forged proof under this bug looks exactly like an honest one [@ecc19]. Groth16&apos;s minimal reference string removed this entire class by construction, because it contains only the elements the proof actually needs [@groth16].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Zcash counterfeiting flaw was &lt;em&gt;not&lt;/em&gt; a leaked secret and &lt;em&gt;not&lt;/em&gt; a failed ceremony. It was a soundness bug in how the reference string was built [@gabizon19][@cve19]. Filing it under &quot;toxic waste&quot; hides the lesson: even a setup whose secrets were perfectly destroyed can be forgeable if the string is structured wrong.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both setup failures, though, share one escape hatch: they vanish entirely if you have no trusted setup at all. Draw every parameter from public randomness and there is no trapdoor to keep and no structured string to get wrong. Bulletproofs does exactly that -- and Bulletproofs was still forged. The trapdoor, it turns out, was never the only seam.&lt;/p&gt;
&lt;h3&gt;4c. The Transcript -- The Frozen Heart&lt;/h3&gt;
&lt;p&gt;The Fiat-Shamir transform is the glue that makes proofs non-interactive, and its soundness hangs on a single question: what goes into the hash? Get that wrong and the whole non-interactive proof becomes forgeable, no matter how strong the hash function is.&lt;/p&gt;

Weak Fiat-Shamir derives the challenge from the prover&apos;s commitment alone -- the challenge is a hash of the commitment. Strong Fiat-Shamir binds the challenge to everything that should matter: a domain separator, the statement, all public inputs, and the full transcript. The entire difference is *what enters the hash*, and it is the difference between a sound proof and a forgeable one.
&lt;p&gt;Here is why the omission is fatal. In a three-move protocol, a simulator that is handed the challenge in advance can fabricate a convincing commitment-and-response pair with no witness at all. That capability is a feature -- it is precisely what makes the protocol zero-knowledge.&lt;/p&gt;
&lt;p&gt;Weak Fiat-Shamir accidentally hands the &lt;em&gt;attacker&lt;/em&gt; the same capability. If the challenge is a hash of the commitment only, a malicious prover can work backwards: choose values so the verification relation holds, for a statement that is false. Because the statement and its public inputs never entered the hash, the transcript no longer pins down &lt;em&gt;which&lt;/em&gt; statement is being proved. What the attacker can reach is protocol-dependent: a random or attacker-derived statement in the general case, and in systems like the Bulletproofs range proof a &lt;em&gt;chosen&lt;/em&gt; out-of-range value -- the case that turns the flaw into forged money.&lt;/p&gt;

flowchart TD
    subgraph Weak[&quot;Weak Fiat-Shamir&quot;]
      W1[Challenge hashes the commitment only] --&amp;gt; W2[Attacker fixes the challenge first]
      W2 --&amp;gt; W3[Back-fills a bogus commitment and response]
      W3 --&amp;gt; W4[Forged proof verifies]
    end
    subgraph Strong[&quot;Strong Fiat-Shamir&quot;]
      S1[Challenge hashes statement, inputs, transcript] --&amp;gt; S2[Challenge pinned to the statement]
      S2 --&amp;gt; S3[No freedom to back-fill]
      S3 --&amp;gt; S4[Forgery blocked]
    end
&lt;p&gt;The tell is that the challenge is &lt;em&gt;identical&lt;/em&gt; for two different statements under weak Fiat-Shamir, so a transcript built for one carries over to another. Watch it happen.&lt;/p&gt;
&lt;p&gt;{`
// A stand-in hash (FNV-1a). Not cryptographic -- illustrative only.
function H(s) {
  let h = 2166136261 &amp;gt;&amp;gt;&amp;gt; 0;
  for (const ch of String(s)) {
    h = Math.imul(h ^ ch.charCodeAt(0), 16777619) &amp;gt;&amp;gt;&amp;gt; 0;
  }
  return h &amp;gt;&amp;gt;&amp;gt; 0;
}&lt;/p&gt;
&lt;p&gt;// Two DIFFERENT statements a prover might try to prove.
const statementA = &quot;pubkeyA-owns-100&quot;;
const statementB = &quot;pubkeyB-owns-100&quot;;
const commitment = &quot;prover-first-message&quot;;&lt;/p&gt;
&lt;p&gt;// WEAK Fiat-Shamir: the challenge ignores the statement entirely.
const weakA = H(commitment);
const weakB = H(commitment);
console.log(&quot;weak FS   -- same challenge for A and B?&quot;, weakA === weakB); // true&lt;/p&gt;
&lt;p&gt;// STRONG Fiat-Shamir: the challenge binds statement + inputs + transcript.
const strongA = H(statementA + &quot;|&quot; + commitment);
const strongB = H(statementB + &quot;|&quot; + commitment);
console.log(&quot;strong FS -- same challenge for A and B?&quot;, strongA === strongB); // false
`}&lt;/p&gt;
&lt;p&gt;Under weak Fiat-Shamir the challenge is the same for statement A and statement B, so any freedom the prover has to satisfy the relation for that one challenge value is not tied to a particular statement -- they can retarget it to a false claim. Strong Fiat-Shamir makes the challenge a commitment to the statement itself, so that freedom is pinned and the retargeting fails.&lt;/p&gt;
&lt;p&gt;This is not a hypothetical, and it is not a one-off. It is the same bug across three eras. In 2012, David Bernhard, Olivier Pereira, and Bogdan Warinschi named the weak-versus-strong distinction and used it to break Helios, a real electronic-voting system that hashed the commitment but not the statement [@bpw12]. A decade later, in April 2022, Trail of Bits found the identical defect living in modern proof-system implementations of Girault&apos;s scheme, Bulletproofs, and PlonK, and gave the class a name [@tob_fh].&lt;/p&gt;

&quot;We&apos;ve dubbed this class of vulnerabilities Frozen Heart. The word frozen is an acronym for FoRging Of ZEro kNowledge proofs.&quot; -- Trail of Bits, coordinated disclosure, April 2022 [@tob_fh]
&lt;p&gt;The disclosure carried the identifier CVE-2022-29566, describing a flawed Fiat-Shamir implementation that does not include all public values in the challenge [@cve22]. The next year, Quang Dao, Jim Miller, Opal Wright, and Paul Grubbs turned the disclosure into a measured corpus: their peer-reviewed study found 36 weak-Fiat-Shamir instances across 12 different proof systems, with concrete knowledge-soundness attacks on Bulletproofs, PlonK, Spartan, and a verifiable-delay function -- including a case that enabled unlimited currency creation in a vulnerable protocol [@dao23].&lt;/p&gt;
&lt;p&gt;The 2022 disclosure reached implementations across many libraries -- iden3&apos;s snarkjs, ConsenSys&apos;s gnark, Dusk Network&apos;s PLonK library, and ZenGo&apos;s zk-paillier among them -- all remediated before publication [@tob_fh].&lt;/p&gt;
&lt;p&gt;Now the sentence that carries the thesis. Bulletproofs has &lt;em&gt;no trusted setup&lt;/em&gt; -- it is transparent, drawing all its randomness from public coins [@stanford_bp]. And it was &lt;em&gt;still&lt;/em&gt; a Frozen Heart victim: Trail of Bits traced the bug to an insecure Fiat-Shamir recommendation in the original Bulletproofs paper itself, which the authors then corrected [@tob_fh][@bulletproofs]. Removing the trapdoor did nothing to protect the transcript, because the two are different seams. The hash was perfectly collision-resistant the entire time. The bug was never in the hash -- it was in what entered it.&lt;/p&gt;
&lt;p&gt;Three layers, three breaks, one striking regularity: not once did the discrete-logarithm, pairing, or hash assumption itself fail. That regularity is not a coincidence. It is a lens.&lt;/p&gt;
&lt;h2&gt;5. Name the Layer, Name the Break&lt;/h2&gt;
&lt;p&gt;Step back from the three incidents and look at the shape they make together. Each one attacked a different part of the artifact -- the circuit, the setup string, the transcript -- and each one lived on the soundness axis. In not one of them did the hardness floor carry the failure. That is not three unrelated stories. It is one lens.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Name the layer -- circuit, setup string, or transcript -- and you have named the break. Across the documented incident record, the hardness floor never carries the failure; the leak is always in the artifact humans compiled on top of it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The lens works because the seams are &lt;em&gt;orthogonal&lt;/em&gt;. They live in different parts of the artifact, so closing one does nothing to the others. This is the second aha moment, and it corrects a comfortable intuition. It is tempting to believe that transparent, no-trusted-setup systems are fundamentally safer -- remove the trapdoor and you remove the risk.&lt;/p&gt;
&lt;p&gt;But Bulletproofs had no trusted setup and was still forgeable through weak Fiat-Shamir [@tob_fh][@stanford_bp]. Removing the setup seam left the transform seam exactly where it was, because the two are independent coordinates. &quot;Name the layer&quot; fully determines &quot;name the break&quot; only because the layers do not bleed into one another.&lt;/p&gt;

flowchart TD
    A[A verifying proof of a false statement] --&amp;gt; B{&quot;Which layer leaked?&quot;}
    B --&amp;gt;|Circuit| C[Under-constrained: a missing constraint]
    B --&amp;gt;|Setup string| D[Trapdoor retained, or a redundant CRS]
    B --&amp;gt;|Transcript| E[Weak Fiat-Shamir, the Frozen Heart]
    B --&amp;gt;|Hardness floor| F[Never observed in the record]
&lt;p&gt;Laid out as a table, the whole incident record fits on a single grid.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Seam&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Canonical incident&lt;/th&gt;
&lt;th&gt;Caught by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Under-constrained circuit&lt;/td&gt;
&lt;td&gt;Arithmetization&lt;/td&gt;
&lt;td&gt;Missing constraint admits multiple witnesses&lt;/td&gt;
&lt;td&gt;Rollup and mixer withdrawal bugs [@sok24]&lt;/td&gt;
&lt;td&gt;Picus, ZKAP, Circomspect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toxic-waste retention&lt;/td&gt;
&lt;td&gt;Setup string&lt;/td&gt;
&lt;td&gt;Retained trapdoor forges any statement&lt;/td&gt;
&lt;td&gt;Groth16 setup, mitigated by ceremony [@groth16]&lt;/td&gt;
&lt;td&gt;MPC ceremony, transparency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redundant reference string&lt;/td&gt;
&lt;td&gt;Setup string&lt;/td&gt;
&lt;td&gt;Malformed string lets a statement be swapped&lt;/td&gt;
&lt;td&gt;Zcash BCTV14 [@cve19]&lt;/td&gt;
&lt;td&gt;Minimal SRS, audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frozen Heart&lt;/td&gt;
&lt;td&gt;Transform&lt;/td&gt;
&lt;td&gt;Weak Fiat-Shamir omits the statement&lt;/td&gt;
&lt;td&gt;Girault, Bulletproofs, PlonK [@cve22]&lt;/td&gt;
&lt;td&gt;Strong-Fiat-Shamir transcripts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The middle two rows are both the setup seam, split into its two distinct modes. Read down the &quot;Caught by&quot; column and you can already see the shape of the defense: one family of tools per layer, which is exactly how the state of the art is organized.&lt;/p&gt;
&lt;p&gt;Complete the picture along the other axis. Nearly every entry in the disclosed corpus is a soundness break, and each maps to exactly one layer of the artifact [@sok24]. The diagonal that matters -- soundness, in the artifact, never the floor -- is the article&apos;s thesis rendered as a coordinate grid.&lt;/p&gt;

The seams are orthogonal: closing one does nothing to the others.
&lt;p&gt;One honest caveat keeps this from becoming triumphalism. The claim is empirical, not a theorem: the hardness floor has held in every documented incident, which means unbroken &lt;em&gt;so far&lt;/em&gt;, not &lt;em&gt;unbreakable&lt;/em&gt;. There is exactly one place where the floor itself is on the clock -- a large quantum computer running Shor&apos;s algorithm against the discrete-logarithm and pairing assumptions -- and we will give that its due in Section 8. For now, the record is unambiguous: the seams leaked; the floor did not.&lt;/p&gt;
&lt;p&gt;If every break has a home layer, then every &lt;em&gt;defense&lt;/em&gt; has a target layer too. So how far has the field gotten at sewing each seam shut?&lt;/p&gt;
&lt;h2&gt;6. Sewing the Seams Shut&lt;/h2&gt;
&lt;p&gt;There is no single &quot;secure zero-knowledge&quot; method, and looking for one misreads the problem. What exists instead is a defense-in-depth stack with one family of tools per seam -- a structure that mirrors the failure taxonomy exactly, because the defenses were built in response to it [@sok24].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The arithmetization seam&lt;/strong&gt; gets the most-developed tooling, because it is the most-exploited layer, and the tools sit on a spectrum that trades soundness against scale.&lt;/p&gt;
&lt;p&gt;At the sound end is Picus, which implements the QED2 algorithm. It formalizes the exact property a missing constraint violates -- that every output signal must be uniquely determined by the inputs -- and then asks a solver the dual question directly: does a second satisfying assignment exist with the same inputs but different outputs? [@picus23] If the solver proves none can, the circuit is deterministic; if it finds one, it returns a concrete pair of witnesses that demonstrates the bug [@picus_gh].&lt;/p&gt;
&lt;p&gt;The catch is that deciding uniqueness over nonlinear field constraints has no efficient general algorithm, so Picus is &lt;em&gt;sound but incomplete&lt;/em&gt;: a clean verdict is trustworthy, but it can time out rather than decide. On a benchmark of 163 Circom circuits it resolved roughly 70 percent [@picus23].&lt;/p&gt;
&lt;p&gt;At the scalable end is ZKAP, which builds a Circuit Dependence Graph and expresses nine vulnerability anti-patterns as queries over it [@zkap24]. It is a bug-finder, not a prover, so it can miss bug shapes nobody encoded -- but it scans large codebases cheaply, and in evaluation it found 81 real vulnerabilities across 258 Circom circuits, several previously unknown and later fixed by developers [@zkap24]. Fastest of all is Circomspect, the Trail of Bits linter that flags the &lt;code&gt;&amp;lt;--&lt;/code&gt; versus &lt;code&gt;&amp;lt;==&lt;/code&gt; slip and other unconstrained-signal patterns at authoring time [@circomspect_gh][@circomspect_blog].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;th&gt;Scaling&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Picus / QED2&lt;/td&gt;
&lt;td&gt;SMT proof of output uniqueness&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sound&lt;/strong&gt;, incomplete&lt;/td&gt;
&lt;td&gt;Weakest (solver cost)&lt;/td&gt;
&lt;td&gt;Determinism proof or exploit witness pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZKAP&lt;/td&gt;
&lt;td&gt;Anti-pattern queries over a dependence graph&lt;/td&gt;
&lt;td&gt;Bug-finder; not sound&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Ranked vulnerability findings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circomspect&lt;/td&gt;
&lt;td&gt;Multi-pass linter&lt;/td&gt;
&lt;td&gt;Heuristic warnings&lt;/td&gt;
&lt;td&gt;Strongest (near-linear)&lt;/td&gt;
&lt;td&gt;Warnings (stdout or SARIF)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Run Circomspect in CI to catch the &lt;code&gt;&amp;lt;--&lt;/code&gt; and &lt;code&gt;&amp;lt;==&lt;/code&gt; slips as they are written, ZKAP for corpus-scale triage, and reserve Picus to &lt;em&gt;prove&lt;/em&gt; your critical circuits deterministic. No single tool is both sound and scalable, so the practical answer is to stack them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The setup seam&lt;/strong&gt; offers a three-way choice rather than a single fix: reduce the trust, amortize it, or remove it. Reduce it with an MPC ceremony, whose modern exemplar is Ethereum&apos;s KZG Summoning Ceremony -- 141,416 contributions over 208 days, secure if any one contributor was honest [@eth_kzg_blog][@eth_ceremony]. Amortize it with a universal, updatable reference string as in PlonK, where one ceremony serves every circuit up to a size bound and new contributors can inject entropy at any time [@plonk]. Or remove the trapdoor outright with a transparent construction.&lt;/p&gt;

A setup that draws only on public randomness -- public coins -- that anyone can reproduce and verify. There is no secret to retain, so there is no trapdoor to leak. By the Goldreich-Oren result it is still a setup; it is simply not a *trusted* one.
&lt;p&gt;Bulletproofs and STARKs are the transparent options: no toxic waste, no ceremony, and no BCTV14-style reference string to get wrong [@bulletproofs][@stark18]. That closes the setup seam completely, at the cost of larger proofs or heavier verification, and -- as we keep seeing -- it does nothing at all for the transform seam.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The transform seam&lt;/strong&gt; is closed by refusing to hand-roll transcripts. Strong-Fiat-Shamir libraries make weak Fiat-Shamir the non-default.&lt;/p&gt;
&lt;p&gt;Merlin, built on the STROBE framework, folds every value into a running transcript through a labeled &lt;code&gt;append_message&lt;/code&gt; call and derives challenges from it, so forgetting to bind a public input becomes a visible omission in the code rather than a silent default [@merlin_gh][@merlin_site].&lt;/p&gt;
&lt;p&gt;Used correctly, a strong-transcript library eliminates the entire &lt;em&gt;avoidable&lt;/em&gt; Frozen Heart class -- every instance that came from an engineer omitting a value from the hash [@merlin_gh]. The honesty this section owes the reader is in the word &quot;avoidable.&quot; No tool here is both sound and scalable; a transcript library can still be misused; a hand-rolled challenge gets no protection at all; and, as Section 8 will show, there is a theoretical ceiling above even a perfectly bound transcript. The defenses shrink the attack surface layer by layer. They do not abolish it.&lt;/p&gt;
&lt;p&gt;These defenses all assume you have already chosen a proof system. But that choice is upstream of everything here -- it decides which seams you inherit before you write a line of circuit. SNARK or STARK: what does each buy, and what does each still owe?&lt;/p&gt;
&lt;h2&gt;7. SNARK or STARK: Which Seams You Inherit&lt;/h2&gt;
&lt;p&gt;The usual comparison of proof systems asks which is fastest or smallest. The thesis reframes the question: &lt;strong&gt;which seam does each construction avoid, and which does it inherit?&lt;/strong&gt; Read that way, the four most-deployed families tell a single clean story.&lt;/p&gt;
&lt;p&gt;Groth16&apos;s proof is small enough -- around 128 to 192 bytes -- to post on-chain cheaply [@groth16], which is why it stays attractive despite needing a fresh ceremony per circuit.&lt;/p&gt;
&lt;p&gt;Groth16 is the smallest and fastest to verify: three group elements, a constant-time pairing check [@groth16]. It inherits the setup seam in its purest form -- per-circuit toxic waste -- and it inherits the transform seam like everything else.&lt;/p&gt;
&lt;p&gt;PlonK keeps the succinct proof but &lt;em&gt;relocates&lt;/em&gt; the setup seam: one universal, updatable reference string for all circuits instead of one per circuit [@plonk]. It relocates the trapdoor; it does not remove it, and it was itself a Frozen Heart victim [@tob_fh]. Bulletproofs goes further and &lt;em&gt;avoids&lt;/em&gt; the setup seam entirely by being transparent [@bulletproofs] -- and was &lt;em&gt;also&lt;/em&gt; a Frozen Heart victim [@tob_fh]. STARKs avoid the setup seam too, and additionally trade the algebraic hardness assumption for a hash-based one.&lt;/p&gt;

A STARK is a scalable, transparent proof system whose security rests only on collision-resistant hashes. It commits to data with Merkle trees and uses FRI -- the Fast Reed-Solomon Interactive Oracle Proof of Proximity -- to test that a committed function is close to a low-degree polynomial. With no algebraic trapdoor it needs no trusted setup, and by dropping discrete-log and pairing assumptions it sheds the structure that Shor&apos;s algorithm targets.
&lt;p&gt;Put them on one grid and read the columns, not the rows.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Groth16 (2016)&lt;/th&gt;
&lt;th&gt;PlonK (2019)&lt;/th&gt;
&lt;th&gt;Bulletproofs (2018)&lt;/th&gt;
&lt;th&gt;STARK / FRI (2018)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Proof size&lt;/td&gt;
&lt;td&gt;3 elements, ~128 to 192 B&lt;/td&gt;
&lt;td&gt;~constant, few hundred B&lt;/td&gt;
&lt;td&gt;$2\log_2 n + 9$ elements&lt;/td&gt;
&lt;td&gt;polylog, tens to hundreds of KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify cost&lt;/td&gt;
&lt;td&gt;~3 pairings (constant)&lt;/td&gt;
&lt;td&gt;~constant&lt;/td&gt;
&lt;td&gt;$O(n)$, batchable&lt;/td&gt;
&lt;td&gt;polylog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup seam&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Inherited&lt;/strong&gt; (per-circuit toxic waste)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Relocated&lt;/strong&gt; (universal trapdoor)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Avoided&lt;/strong&gt; (transparent)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Avoided&lt;/strong&gt; (transparent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transform seam&lt;/td&gt;
&lt;td&gt;Inherited&lt;/td&gt;
&lt;td&gt;Inherited (Frozen Heart)&lt;/td&gt;
&lt;td&gt;Inherited (Frozen Heart)&lt;/td&gt;
&lt;td&gt;Inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardness floor&lt;/td&gt;
&lt;td&gt;Pairings (Shor-breakable)&lt;/td&gt;
&lt;td&gt;Pairings + KZG (Shor-breakable)&lt;/td&gt;
&lt;td&gt;Discrete log (Shor-breakable)&lt;/td&gt;
&lt;td&gt;Hashes (plausibly PQ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-quantum&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Conjectural&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Move left to right along the setup row and the seam closes: inherited, relocated, avoided, avoided [@groth16][@plonk][@bulletproofs][@stark18]. Now read the transform row: inherited, inherited, inherited, inherited -- all the way across, transparent systems included. That one unbroken column is the entire argument in miniature. Closing the trapdoor does nothing for weak Fiat-Shamir, because they are different seams in the same artifact. And the hardness-floor row never once carries the break; the only future risk it names is the Shor entry, ticking for every algebraic system on the left.&lt;/p&gt;
&lt;p&gt;Two disciplined caveats keep this table honest.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For STARKs, &quot;post-quantum secure&quot; is a &lt;em&gt;conjecture&lt;/em&gt;. Security rests on the conjectured collision resistance of a hash, which no one has proved survives a quantum adversary, and Grover&apos;s algorithm forces larger hash parameters to hold the same margin [@stark18]. Write &quot;plausibly post-quantum,&quot; never &quot;post-quantum secure&quot; as if it were settled.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The second caveat is about the word &quot;no.&quot; Transparent cores are often wrapped for deployment.&lt;/p&gt;
&lt;p&gt;Production STARK pipelines frequently wrap a final Groth16 proof around the STARK so the on-chain verifier checks only a tiny constant-size proof, exactly as RISC Zero&apos;s zkVM does when it compresses its final succinct STARK receipt into an on-chain Groth16 receipt [@risc0recursion]. That last mile re-introduces a trusted setup at the recursion boundary. So &quot;no trusted setup&quot; is true of the STARK &lt;em&gt;core&lt;/em&gt;, not necessarily of the whole &lt;em&gt;pipeline&lt;/em&gt; -- a re-introduced surface by construction, not a documented exploited break.&lt;/p&gt;

Ariel Gabizon sits on both sides of this story. He analyzed the BCTV14 redundant-reference-string counterfeiting bug that threatened Zcash [@gabizon19], and he co-authored PlonK, one of the constructions that relocated the setup seam [@plonk]. The expert who found a break also built a defense -- this is a field that advances by adversarial pressure, not a morality tale with heroes and villains.
&lt;p&gt;How each of these systems is actually built -- the QAP compilation, the FRI protocol, the polynomial commitments -- is the subject of the companion article &quot;&lt;a href=&quot;https://paragmali.com/blog/never-decrypted-proving-you-ran-the-computation-without-reve/&quot; rel=&quot;noopener&quot;&gt;Never Decrypted: Proving You Ran the Computation&lt;/a&gt;.&quot; Here we only need enough to see where each one leaks.&lt;/p&gt;
&lt;p&gt;Transparency, strong transcripts, sound circuits: with enough care, can we close every seam? The theorems say no -- and they say precisely how far &quot;careful&quot; is even allowed to go.&lt;/p&gt;
&lt;h2&gt;8. What Being Careful Cannot Fix&lt;/h2&gt;
&lt;p&gt;Some of the failures in this article are not sloppiness. They are pinned by theorems that tell you how careful you are even &lt;em&gt;allowed&lt;/em&gt; to be. Four bounds define the ceiling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Soundness error is never exactly zero.&lt;/strong&gt; In the interactive-proof model, a cheating prover convinces the verifier of a false statement with some probability, and you can shrink that probability but never eliminate it for a non-trivial statement [@gmr85].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; There is no non-trivial proof system with zero soundness error. You drive the error down toward $2^{-k}$ by enlarging the challenge space or repeating rounds, but never to exactly zero [@gmr85]. &quot;Sound&quot; is always shorthand for &quot;sound except with negligible probability.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Setup is necessary.&lt;/strong&gt; Goldreich and Oren proved that non-trivial non-interactive zero-knowledge in the plain model collapses to BPP [@go94]. Transparency can downgrade a &lt;em&gt;trusted&lt;/em&gt; setup to a public-coin one, but &quot;no setup at all&quot; is unreachable. This is the theorem behind the whole &quot;no &lt;em&gt;trusted&lt;/em&gt; setup, never no setup&quot; discipline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fiat-Shamir is uninstantiable in the standard model.&lt;/strong&gt; In 2003, Shafi Goldwasser and Yael Tauman Kalai showed there exists a secure interactive protocol whose Fiat-Shamir version is insecure for &lt;em&gt;every&lt;/em&gt; concrete hash function [@gk03]. Strong-transcript engineering removes the &lt;em&gt;avoidable&lt;/em&gt; weak-Fiat-Shamir failures, but it cannot lift this ceiling: even a perfectly bound transcript rests on the random-oracle heuristic, not a proof.&lt;/p&gt;
&lt;p&gt;For two decades that impossibility was a contrived, almost pathological construction. Then it got real.&lt;/p&gt;

In 2025, Dmitry Khovratovich, Ron Rothblum, and Lev Soukhanov moved the 2003 impossibility from a toy protocol to a *practical* soundness attack on a real, deployed GKR-based proof system, generating accepting proofs for false statements for every choice of Fiat-Shamir hash function [@khov25]. Quanta Magazine described it as shaking the bedrock assumption -- proving lies no matter which hash is used [@quanta25]. The guardrail matters: this break lives in the Fiat-Shamir *heuristic*, the transform layer, **not** in the hash&apos;s collision resistance. The hardness floor still stands; it is the transform that is now provably crackable in a real case.
&lt;p&gt;That is the third aha moment. The intuition it corrects is the belief that disciplined engineering -- strong transcripts, sound circuits, a clean ceremony -- could in principle make a proof system perfectly and provably sound. The theorems say otherwise: soundness error is irreducible, setup is unavoidable, and Fiat-Shamir soundness has no standard-model proof and now a working counterexample on a real system. Yet even here, at the frontier where the news is worst, the break is in the artifact&apos;s heuristic layer, not the discrete-log or hash hardness underneath.&lt;/p&gt;
&lt;p&gt;There is exactly one place the hardness floor itself is genuinely on the clock, and intellectual honesty requires naming it. A large quantum computer running &lt;a href=&quot;https://paragmali.com/blog/how-q-day-breaks-everything-shors-algorithm-and-the-simultan/&quot; rel=&quot;noopener&quot;&gt;Shor&apos;s algorithm&lt;/a&gt; breaks the discrete-logarithm and pairing assumptions that underpin Groth16, PlonK, and Bulletproofs [@stark18]. That is Q-Day: the one scenario in which the floor, not a seam, gives way. STARKs are the field&apos;s hedge against it -- they shed the algebraic target for a hash-based one [@stark18] -- but their post-quantum guarantee is a conjecture, not a theorem.&lt;/p&gt;

A strengthening of plain soundness: it demands that any prover who produces a verifying proof must actually *know* a witness, formalized by an extractor that can recover that witness from the prover. Many modern SNARK attacks are knowledge-soundness attacks -- the prover convinces the verifier without knowing any valid witness at all.
&lt;p&gt;Grover&apos;s algorithm gives a quadratic speedup on brute-force search, which effectively halves a hash function&apos;s security margin. Post-quantum parameters must roughly double the hash output size to hold the same level of security -- one reason STARK proofs are large.&lt;/p&gt;
&lt;p&gt;Assemble the four bounds and the perfect proof system dissolves as a target. It would be constant-size, transparent, unconditionally post-quantum, and mechanically proven both sound and correctly constrained end to end. Goldreich-Oren forbids the &quot;no setup&quot; part; Goldwasser-Kalai forbids the standard-model soundness proof that non-interactivity would need; and no construction has a &lt;em&gt;theorem&lt;/em&gt; of post-quantum security, only a conjecture with Grover eating into the margin.&lt;/p&gt;
&lt;p&gt;The reachable ideal is not a single global optimum. It is a stack of local optima -- sound-verified circuits, transparent setup, strong transcripts, post-quantum-posture hashes -- each seam sewn as tight as its own theorem allows.&lt;/p&gt;
&lt;p&gt;If the perfect system is provably out of reach, the honest question is not &quot;is it solved?&quot; but &quot;where, exactly, are the seams still open?&quot;&lt;/p&gt;
&lt;h2&gt;9. Where the Seams Are Still Open&lt;/h2&gt;
&lt;p&gt;Five frontiers are visibly moving, and each one is a seam not yet fully sewn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sound and scalable circuit verification.&lt;/strong&gt; The dominant bug class deserves a tool that is both sound and fast, and none exists: Picus proves determinism but times out on large circuits, while ZKAP and Circomspect scale but are incomplete [@picus23][@zkap24][@sok24]. Production circuits run to millions or billions of constraints; the tool that could certify determinism at that scale would close the seam attackers use most.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closing the Fiat-Shamir theory-practice gap.&lt;/strong&gt; Strong transcripts eliminate the &lt;em&gt;avoidable&lt;/em&gt; weak-Fiat-Shamir bugs, but deployed non-interactive soundness still rests on the random-oracle heuristic, and that gap is widening rather than closing -- the class recurred for a decade, then the 2025 result made the theoretical impossibility a working attack on a real system [@dao23][@gk03][@khov25]. The open goal is a standardized, machine-checkable transcript specification that provably rules out weak instances, or correlation-intractable hashes that instantiate Fiat-Shamir soundly for a practically useful class of protocols.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trust-minimized setup at Groth16&apos;s profile.&lt;/strong&gt; Transparent systems remove the trapdoor but pay in proof size, verification cost, or a merely conjectural post-quantum guarantee. No construction today matches Groth16&apos;s roughly 128-byte, constant-verify profile &lt;em&gt;and&lt;/em&gt; transparency &lt;em&gt;and&lt;/em&gt; proven post-quantum security at once [@plonk][@stark18][@eth_kzg_blog]. Goldreich-Oren forbids &quot;no setup,&quot; so the reachable target is &quot;no &lt;em&gt;trusted&lt;/em&gt; setup&quot; -- and the race is to close the size-and-speed penalty.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provably, not conjecturally, post-quantum arguments.&lt;/strong&gt; Every deployed pairing or discrete-log SNARK is on Shor&apos;s clock, and the hash-based hedge trades that for a conjecture with Grover eating the margin [@stark18]. A succinct argument with an &lt;em&gt;unconditional&lt;/em&gt; post-quantum soundness theorem -- not an assumption about a hash -- does not yet exist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit tooling that keeps pace with new arithmetizations.&lt;/strong&gt; The deepest tooling targets Circom and R1CS, but constructive innovation is racing ahead into AIR, Plonkish custom gates, and full zkVM instruction traces -- re-opening the same arithmetization and transform seams in new syntaxes, including the GKR-inside-a-zkVM setting where the 2025 Fiat-Shamir break actually landed [@sok24][@khov25]. Porting sound analysis to those targets is early-stage work.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The gap between &quot;we linted it&quot; and &quot;we proved it deterministic&quot; is not a footnote -- it is the everyday condition of zero-knowledge auditing. Sound tools do not scale; scalable tools are not sound. Every production audit lives in that gap and closes it with human judgment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Until these frontiers close, the most reliable defense is not a tool at all. It is a disciplined human review -- one that walks the same three layers, in the same order the bugs appear.&lt;/p&gt;
&lt;h2&gt;10. A Layer-by-Layer Design Review&lt;/h2&gt;
&lt;p&gt;Everything in this article compresses into a checklist a reviewer can apply cold: three layers, three questions, asked in the order the bugs actually appear.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1, arithmetization.&lt;/strong&gt; Is &lt;em&gt;every&lt;/em&gt; signal constrained, with no stray &lt;code&gt;&amp;lt;--&lt;/code&gt; where a &lt;code&gt;&amp;lt;==&lt;/code&gt; was intended? Are the outputs uniquely determined by the inputs on the critical path -- provably, not by inspection? Are there aliasing gaps or non-deterministic &quot;hint&quot; signals trusted without an in-circuit check? Run Circomspect in CI to catch the assignment-versus-constraint slips [@circomspect_gh], ZKAP for corpus-scale triage [@zkap24], and Picus to prove determinism on the circuits that guard funds [@picus23]. The property Picus checks is the one you can test yourself in miniature.&lt;/p&gt;
&lt;p&gt;{`
// Echoes the question Picus/QED2 asks: for fixed inputs, is the output unique?
const p = 13;
const mod = (a) =&amp;gt; ((a % p) + p) % p;&lt;/p&gt;
&lt;p&gt;// The author intends &quot;out = 1 exactly when inp is nonzero&quot;, but writes only:
//        inp * out === inp
// which forgets to force out to be boolean. Watch input 0 go wrong.
function constraintHolds(inp, out) {
  return mod(inp * out) === mod(inp);
}&lt;/p&gt;
&lt;p&gt;function acceptedOutputs(inp) {
  const outs = [];
  for (let out = 0; out &amp;lt; p; out++) {
    if (constraintHolds(inp, out)) outs.push(out);
  }
  return outs;
}&lt;/p&gt;
&lt;p&gt;for (let inp = 0; inp &amp;lt;= 2; inp++) {
  console.log(&quot;input&quot;, inp, &quot;-&amp;gt; accepted outputs&quot;, acceptedOutputs(inp));
}
// input 0 accepts EVERY output 0..12 -- under-determined and forgeable.
// inputs 1 and 2 accept exactly one output -- uniquely determined.
// A determinism checker flags input 0; a plain linter never would.
`}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 2, setup.&lt;/strong&gt; Is the system trusted or transparent? If trusted, was the ceremony run with at least one credibly honest party, and is your reference string byte-identical to the audited transcript you think you are using? [@eth_kzg_blog] Remember that a universal SRS is not the same as no trapdoor -- PlonK&apos;s string is reusable, not trustless [@plonk]. Prefer transparency where your proof-size and verification budget allows it; otherwise, verify the ceremony rather than assuming it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 3, transform.&lt;/strong&gt; Does the challenge hash bind the domain separator, the statement, &lt;em&gt;all&lt;/em&gt; public inputs, and the full transcript? Use an audited transcript library such as Merlin rather than a hand-rolled hash of the commitment [@merlin_gh]. This is the single question that would have prevented every Frozen Heart instance.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 1. &lt;strong&gt;Circuit:&lt;/strong&gt; Is every signal constrained, and is every output uniquely determined by the inputs? 2. &lt;strong&gt;Setup:&lt;/strong&gt; Trusted or transparent -- and if trusted, is your string the audited one and the ceremony credibly honest? 3. &lt;strong&gt;Transcript:&lt;/strong&gt; Does the challenge hash bind the statement and every public input? Three questions, one per seam, in the order bugs appear.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is a fourth question that sits above the other three: should this be a zero-knowledge system at all?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Zero-knowledge adds three failure layers -- circuit, setup, transcript -- above a hardness floor that, on its own, was never the problem. If you need neither privacy nor succinct verification of an untrusted computation, a signature or a re-execution has none of those seams to get wrong. Adopt zero-knowledge only when its guarantees are the ones you actually need.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is not an argument against zero-knowledge proofs. They do something nothing else can -- verify a computation you never watched, while learning nothing about its secrets. It is an argument for adopting them with open eyes, knowing that each of their conveniences is also a seam.&lt;/p&gt;
&lt;p&gt;The checklist works for one reason: every question maps to a layer, and every layer, we now know, is where the breaks actually live. That is the thesis restated as an operating procedure.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;


Not by itself. Soundness is defined relative to the *circuit&apos;s* constraints, not your intentions. If the circuit is under-constrained, it will certify witnesses the specification never meant to allow, and the verifier accepts them as valid [@sok24]. &quot;Valid&quot; means &quot;some witness satisfied the constraints,&quot; which equals &quot;true&quot; only when the constraints were written to force it.


No. The setup trapdoor governs *soundness*, not privacy. Whoever retains it can forge proofs of false statements [@groth16], but it tells them nothing about what any honest proof was hiding. Confusing the two misplaces the risk: the trapdoor is a forgery risk, not a de-anonymization one.


Conjecturally, not provably. STARK security rests on the conjectured collision resistance of a hash, which no one has proved survives a quantum adversary, and Grover&apos;s algorithm forces larger hash parameters to keep the same margin [@stark18]. The correct phrase is &quot;plausibly post-quantum,&quot; never &quot;post-quantum secure&quot; as if it were a theorem.


Almost never. In the documented record, the break is in the layer above the hardness assumption -- the circuit, the setup string, or the transcript -- while discrete logarithm, pairings, and hashes held [@sok24]. Name the layer and you have named the break; the floor rarely enters the story.


No, and that distinction is the point. Timing attacks, faulty randomness, and nonce leakage are real, but they are implementation faults -- the subject of the sibling article &quot;How Zero-Knowledge Broke in Real Life.&quot; Everything here is *structural*: it would still be a bug on perfect hardware with a perfect random-number generator.


Because the transform seam is orthogonal to the setup seam. Bulletproofs is transparent, yet it was still a Frozen Heart victim through weak Fiat-Shamir, traced to an insecure recommendation in the original paper [@tob_fh][@bulletproofs]. Removing the trapdoor does nothing about what enters the challenge hash.


Not yet. No deployed tool is both sound and scalable: Picus proves determinism but times out on large circuits, while ZKAP and Circomspect scale but are incomplete [@picus23][@zkap24]. Production audits close that gap with human review, not tooling alone.

&lt;h2&gt;12. Making &quot;Verified&quot; Mean What You Think&lt;/h2&gt;
&lt;p&gt;Return one last time to the verifier that returned &lt;code&gt;true&lt;/code&gt;. Nothing about that bit was wrong. The pairing balanced, the hash matched, and the assumption underneath held -- as it has held in every documented break in this article. What failed was the artifact humans compiled on top of that floor: a circuit missing a constraint, a setup string built or handled wrong, a transcript that hashed too little.&lt;/p&gt;
&lt;p&gt;That is the whole argument, and the evidence now sits behind it. Across the disclosed record, the hardness floor never carried the failure [@sok24]. The three seams above it did -- and each break announced its own layer. Under-constrained circuits forge in the arithmetization; retained or malformed setup strings forge in the setup; weak Fiat-Shamir forges in the transform.&lt;/p&gt;
&lt;p&gt;Even a transparent, hash-based STARK only sheds the setup seam while inheriting the rest, and its post-quantum promise is a conjecture with Grover&apos;s clock running. The one place the floor itself is genuinely at risk is Q-Day, when Shor&apos;s algorithm reaches the algebraic assumptions -- the single scenario this article cannot file under &quot;the layer humans built.&quot;&lt;/p&gt;

The cryptography was never the weak point. The discipline is making sure that when the verifier says &quot;true,&quot; it means the thing you actually intended -- constraint by constraint, string by string, hash input by hash input.
&lt;p&gt;Name the layer, and you have named the break. For how these systems are constructed in the first place, see &quot;Never Decrypted: Proving You Ran the Computation.&quot; For the implementation and side-channel failures deliberately held out of scope here, see &quot;How Zero-Knowledge Broke in Real Life.&quot; Both are the other half of learning to trust a proof you did not watch being made.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;how-zero-knowledge-proofs-actually-break&quot; keyTerms={[
  { term: &quot;Soundness&quot;, definition: &quot;The guarantee that a cheating prover cannot convince an honest verifier of a false statement except with negligible probability; the property attackers target.&quot; },
  { term: &quot;Under-constrained circuit&quot;, definition: &quot;A constraint system that admits more than one witness for a signal the designer meant to pin down, letting a prover certify an out-of-spec value.&quot; },
  { term: &quot;Toxic waste&quot;, definition: &quot;The secret scalars a trusted setup samples and must destroy; whoever retains them holds a trapdoor that forges any statement.&quot; },
  { term: &quot;Fiat-Shamir transform&quot;, definition: &quot;Replacing a verifier&apos;s random challenge with a hash of the transcript to make a proof non-interactive; sound only when the hash binds the statement and all public inputs.&quot; },
  { term: &quot;Transparent setup&quot;, definition: &quot;A setup using only public randomness, with no secret trapdoor to leak; still a setup, just not a trusted one.&quot; },
  { term: &quot;Knowledge soundness&quot;, definition: &quot;A strengthening of soundness requiring that any prover who convinces the verifier must actually know a witness, formalized by an extractor.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>zero-knowledge-proofs</category><category>zk-snarks</category><category>cryptography</category><category>soundness</category><category>fiat-shamir</category><category>trusted-setup</category><category>circuit-security</category><category>zk-starks</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Discrete Log Held; The Proofs Leaked: A Field Guide to Commitments and Sigma Protocols</title><link>https://paragmali.com/blog/the-discrete-log-held-the-proofs-leaked-a-field-guide-to-com/</link><guid isPermaLink="true">https://paragmali.com/blog/the-discrete-log-held-the-proofs-leaked-a-field-guide-to-com/</guid><description>How the discrete log stayed hard for forty years while the proofs built on it kept breaking: Pedersen commitments, Schnorr, Sigma protocols, and Fiat-Shamir.</description><pubDate>Mon, 13 Jul 2026 20:58:24 GMT</pubDate><content:encoded>
A discrete-log zero-knowledge proof is one three-move conversation -- commit, challenge, respond -- and for forty years every real break landed on one of those moves, never on the discrete logarithm underneath. Move 1, the commitment: Pedersen hides perfectly and binds on the discrete log, but a trapdoor-holder can equivocate. Move 2, the Fiat-Shamir challenge: hash too little and you get weak Fiat-Shamir, the Frozen Heart bug that forged &quot;zero-knowledge&quot; proofs in Helios (2012) and in Bulletproofs and PlonK (2022) with the math untouched. Move 3, the response: reuse the nonce and the witness falls out by algebra; exploit its linearity across concurrent sessions and the ROS attack forges naive multisignatures in polynomial time. This is the field guide to those three moves -- Pedersen, Schnorr, Sigma protocols, Fiat-Shamir -- how they compose into Bulletproofs, MuSig2, FROST, Signal&apos;s credentials, and Privacy Pass, and every documented way each has been gotten wrong.
&lt;h2&gt;1. The Math Was Fine. The Proofs Were Forgeable.&lt;/h2&gt;
&lt;p&gt;In April 2022, Trail of Bits published a coordinated disclosure with an unsettling shape [@tob-part1]. Across three unrelated &quot;zero-knowledge&quot; proof systems -- Girault&apos;s classic proof of knowledge, Bulletproofs (the range proof securing confidential Monero transactions), and PlonK (a modern general-purpose SNARK) -- the proofs were &lt;em&gt;forgeable&lt;/em&gt;. An attacker could produce a convincing proof of a statement that was simply false [@tob-part1], [@tob-girault], [@tob-bp], [@tob-plonk].&lt;/p&gt;
&lt;p&gt;And in none of the three cases had anyone broken the discrete logarithm, or factoring, or any hard problem. The math was untouched. The bug was that a single hash had omitted part of the statement it was supposed to be about [@tob-part1].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In every Frozen Heart case, the discrete logarithm was never solved. The forgery lived entirely in what a hash function failed to bind. Soundness turned out to be a property of the protocol&apos;s plumbing, not of the hard problem beneath it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Bulletproofs instance was the sharpest. Trail of Bits traced it to an insecure Fiat-Shamir generation &lt;em&gt;recommended in the original 2017 Bulletproofs paper itself&lt;/em&gt; -- the flaw was in the specification, not a careless implementer&apos;s shortcut [@tob-bp], [@bulletproofs]. It received a CVE: CVE-2022-29566 [@cve]. A vulnerability class was born, and Trail of Bits named it. The word &quot;Frozen&quot; is the acronym -- FoRging Of ZEro-kNowledge proofs; &quot;Heart&quot; nods to the Fiat-Shamir transform that sits at the heart of these proof systems [@tob-part1].&lt;/p&gt;
&lt;p&gt;Then the twist that makes it a pattern, not an incident: this exact bug had already been named and published a decade earlier. In 2012, David Bernhard, Olivier Pereira, and Bogdan Warinschi showed that the Helios internet-voting system used the same shortcut -- and that its &quot;zero-knowledge&quot; ballot proofs could be transplanted and forged the same way [@bpw]. Two breaks, ten years apart, identical root cause, and the second one shipped in production behind a CVE.&lt;/p&gt;
&lt;p&gt;Now widen the aperture. The discrete logarithm -- the hard problem under Diffie-Hellman, Schnorr, ElGamal, and the whole family -- has resisted forty years of attack; &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/&quot; rel=&quot;noopener&quot;&gt;nobody broke the discrete log&lt;/a&gt;, as Part 18 argued at length. Yet the proof systems built on top of it break with some regularity. Why?&lt;/p&gt;
&lt;p&gt;Because a discrete-log zero-knowledge proof is not one thing. It is a three-move conversation -- &lt;strong&gt;commit, challenge, respond&lt;/strong&gt; -- and each move is a separate attack surface. The prover sends a commitment, the verifier sends a random challenge, the prover sends a response, and one equation decides the outcome.&lt;/p&gt;
&lt;p&gt;Frozen Heart and Helios are failures of the second move, the challenge. Reusing a signature nonce -- the catastrophe that &lt;a href=&quot;https://paragmali.com/blog/one-number-used-twice-how-a-repeated-nonce-hands-over-your-p/&quot; rel=&quot;noopener&quot;&gt;hands over a private key when one number repeats&lt;/a&gt; (Part 8) -- is a failure of the third move, the response. A trapdoor that lets a committer change its mind after the fact is a failure of the first move, the commitment. Three moves, three ways to leak, and not one of them is the discrete logarithm.&lt;/p&gt;
&lt;p&gt;That is the whole thesis of this article, stated once so you can hold it through everything that follows: &lt;strong&gt;the discrete log held; the three moves leaked.&lt;/strong&gt; The hard problem underneath these systems has never fallen. The conversation built on top of it has failed, repeatedly, in ways that are precise, named, and preventable.&lt;/p&gt;
&lt;p&gt;The reach of these three moves is easy to underestimate. The same commit-challenge-respond skeleton runs under Bitcoin&apos;s Taproot multisignatures, under Monero&apos;s confidential transaction amounts, under Signal&apos;s private group membership, and under the digital-identity credentials now standardizing at the IETF. Master the three moves and you have a lens that resolves an entire branch of applied cryptography -- and a checklist that separates a deployment that ships from one that ships a CVE.&lt;/p&gt;
&lt;p&gt;Those three moves were not invented together, and the cleanest one was invented first and deployed last -- frozen for nearly two decades by a patent [@schnorr-patent]. To see why the conversation has exactly three moves, and why each one leaks, we start where the vocabulary was born.&lt;/p&gt;
&lt;h2&gt;2. The Three-Move Conversation&lt;/h2&gt;
&lt;p&gt;Strip away the applications -- the multisignatures, the range proofs, the credentials -- and there is one shape underneath all of them. Learn that shape precisely, and every guarantee and every failure in this article becomes a statement about one of its three parts.&lt;/p&gt;
&lt;p&gt;The archetype is Schnorr&apos;s protocol for proving you know a discrete logarithm. You have a public key $y = g^x$ in a group of prime order $q$ generated by $g$, and you want to convince a verifier that you know the secret exponent $x$ without revealing it. The conversation has exactly three moves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Commit.&lt;/strong&gt; Pick a fresh random nonce $r$ and send the commitment $a = g^r$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Challenge.&lt;/strong&gt; The verifier picks a random $e$ and sends it back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respond.&lt;/strong&gt; Send $z = r + e\cdot x$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The verifier accepts if and only if $g^z = a\cdot y^e$. Check the algebra: $g^{r + e x} = g^r\cdot (g^x)^e = a\cdot y^e$. It works whenever the prover really knows $x$.&lt;/p&gt;

A three-move public-coin proof -- commitment, then a random challenge, then a response -- in which the verifier checks a single equation. It must satisfy two properties: special soundness and special honest-verifier zero-knowledge. Schnorr&apos;s proof of knowledge of a discrete logarithm is the archetype [@damgard].

sequenceDiagram
    participant P as Prover (knows x)
    participant V as Verifier (knows y)
    Note over P: pick fresh random nonce r
    Note over P: commitment a holds g to the r
    P-&amp;gt;&amp;gt;V: send commitment a
    Note over V: pick random challenge e
    V-&amp;gt;&amp;gt;P: send challenge e
    Note over P: response z holds r plus e times x
    P-&amp;gt;&amp;gt;V: send response z
    Note over V: check g to the z equals a times y to the e
&lt;p&gt;Why is this a &lt;em&gt;proof&lt;/em&gt;, and why does it reveal nothing? Both answers are visible by inspection, which is exactly why Schnorr is the teaching example.&lt;/p&gt;
&lt;h3&gt;Two accepting transcripts hand you the secret&lt;/h3&gt;
&lt;p&gt;Suppose a prover produces two accepting conversations that share the same commitment $a$ but have different challenges $e_1 \ne e_2$, with responses $z_1$ and $z_2$. Then $g^{z_1} = a\cdot y^{e_1}$ and $g^{z_2} = a\cdot y^{e_2}$. Divide the two equations and the $a$ cancels: $g^{z_1 - z_2} = y^{e_1 - e_2}$. Since $y = g^x$, the exponents must match, and you can solve for the witness:&lt;/p&gt;
&lt;p&gt;$$x = \frac{z_1 - z_2}{e_1 - e_2} \pmod q.$$&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;special soundness&lt;/strong&gt;, and the algorithm that recovers $x$ is called the &lt;em&gt;extractor&lt;/em&gt;. It is what makes Schnorr a proof of &lt;em&gt;knowledge&lt;/em&gt;: a prover who can answer two different challenges on the same commitment demonstrably possesses $x$, because we could run the extractor and read it out [@damgard].&lt;/p&gt;

The property that two accepting transcripts sharing a commitment but with different challenges yield the witness through an efficient extractor. It is what upgrades a Sigma protocol from &quot;the statement is true&quot; to &quot;the prover actually knows a witness&quot; [@damgard].
&lt;p&gt;Hold on to that extractor. In Section 6 we will watch an attacker run exactly this formula -- same $a$, two different $e$ values -- against a signer who reused a nonce, and read out the private key. The defender&apos;s soundness proof and the attacker&apos;s key-recovery are the same three lines of algebra.&lt;/p&gt;
&lt;h3&gt;One accepting transcript hands you nothing&lt;/h3&gt;
&lt;p&gt;Now run the trick the other way. Pick the response $z$ and the challenge $e$ &lt;em&gt;first&lt;/em&gt;, both at random, and then define the commitment as $a = g^z\cdot y^{-e}$. By construction $g^z = a\cdot y^e$, so this transcript passes verification perfectly -- and you built it without knowing $x$ at all. The distribution of $(a, e, z)$ produced this way is identical to a real run.&lt;/p&gt;

The property that any accepting transcript can be produced without the witness by choosing the response and challenge first and back-solving for the commitment. The algorithm that does this is the simulator, and its existence proves the exchange reveals nothing about the secret [@damgard].
&lt;p&gt;That algorithm is the &lt;em&gt;simulator&lt;/em&gt;, and its existence is the proof that the conversation leaks nothing about $x$: anything a verifier sees, it could have generated by itself. The whole edifice of zero knowledge -- the idea that &quot;reveals nothing&quot; can be made mathematically precise by demanding a simulator -- traces to Goldwasser, Micali, and Rackoff in 1985 [@gmr]. Part 1 covers those &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;security definitions&lt;/a&gt; in full; here we take them as the bedrock they are.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A Sigma protocol is one three-move conversation -- commit, challenge, respond -- whose two guarantees are both visible by inspection. Two accepting transcripts on the same commitment extract the secret (special soundness); a single transcript can be simulated without the secret (special honest-verifier zero knowledge). Two transcripts hand you the key; one transcript hands you nothing. That knife-edge is the entire game.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The distinction the extractor forces is worth naming. A Sigma protocol is a &lt;strong&gt;proof of knowledge&lt;/strong&gt; -- the prover provably &lt;em&gt;possesses&lt;/em&gt; a witness -- not merely a &lt;strong&gt;proof of membership&lt;/strong&gt;, which would only assert that the statement is true. The gap matters: &quot;this public key has a corresponding private key&quot; is trivially true for every well-formed key, but &quot;I know that private key&quot; is the thing a signature must establish.&lt;/p&gt;

A proof that the prover actually possesses a witness, formalized by the existence of an extractor that could recover the witness from a successful prover. It is strictly stronger than a proof of membership, which asserts only that the statement is true [@damgard].

The simulator is not just a proof device; it is a construction tool. To prove &quot;I know the secret behind public key A OR the one behind B&quot; without revealing which, the prover *honestly* runs the branch it can answer and *simulates* the branch it cannot -- choosing that branch&apos;s challenge and response first and back-solving for its commitment, exactly as SHVZK allows. A shared challenge, split between the branches, ties them together so a cheater cannot simulate both at once. That is the Cramer-Damgard-Schoenmakers OR-proof, and it is why the toolkit composes into disjunctions, ring signatures, and selective-disclosure credentials [@cds].
&lt;p&gt;Here is that whole story in code. The extractor is not a metaphor; it is a five-line function that turns two transcripts into a private key.&lt;/p&gt;
&lt;p&gt;{`
// A toy prime-order group: the order-q subgroup of the integers mod p.
// p = 2039 is prime, q = 1019 is prime, and g = 4 has order exactly q.
const p = 2039n, q = 1019n, g = 4n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function powmod(base, exp, m) {
  let r = 1n; base = mod(base, m);
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = mod(r * base, m); base = mod(base * base, m); exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
const invModQ = (a) =&amp;gt; powmod(a, q - 2n, q);   // inverse mod prime q, via Fermat&lt;/p&gt;
&lt;p&gt;const x = 777n;                                 // the secret witness (never sent)
const y = powmod(g, x, p);                      // public key y = g^x&lt;/p&gt;
&lt;p&gt;// One honest run with nonce r and challenge e:
function transcript(r, e) {
  const a = powmod(g, r, p);                    // commit
  const z = mod(r + e * x, q);                  // respond
  return { a, e, z };
}
const verify = (t) =&amp;gt; powmod(g, t.z, p) === mod(t.a * powmod(y, t.e, p), p);&lt;/p&gt;
&lt;p&gt;const r = 314n;                                 // ONE nonce, reused across two challenges
const t1 = transcript(r, 11n);
const t2 = transcript(r, 90n);                  // same a, different e -- the fatal pair
console.log(&apos;t1 verifies = &apos; + verify(t1) + &apos;, t2 verifies = &apos; + verify(t2));&lt;/p&gt;
&lt;p&gt;// The extractor: x = (z1 - z2) / (e1 - e2) mod q
const xRecovered = mod((t1.z - t2.z) * invModQ(mod(t1.e - t2.e, q)), q);
console.log(&apos;recovered x = &apos; + xRecovered + &apos;, true x = &apos; + x + &apos;, match = &apos; + (xRecovered === x));
`}&lt;/p&gt;
&lt;p&gt;That is the knife-edge in code: two transcripts hand you the key, one hands you nothing -- so every guarantee and every failure ahead is a statement about one of the three moves. But this clean picture took eleven years and five separate ideas to assemble, and the cleanest idea of all sat frozen behind a patent.&lt;/p&gt;
&lt;h2&gt;3. Five Ideas and a Patent (1985-1996)&lt;/h2&gt;
&lt;p&gt;The three-move conversation was assembled one idea at a time, mostly by people who were not trying to build it. And the sharpest tension in the story is this: the cleanest, smallest scheme in the whole toolkit was invented in 1989 and could not be legally deployed at scale until its patent expired around 2008 [@schnorr-patent]. The best piece shipped last. Here is how five disconnected papers became a coherent toolkit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Goldwasser, Micali, and Rackoff (1985)&lt;/strong&gt; gave the field its vocabulary. Their paper on the knowledge complexity of interactive proof systems defined zero knowledge through the simulator argument -- a proof reveals nothing if a simulator can produce the same transcript without the secret [@gmr]. Without this definition, none of the three moves would be analyzable; &quot;reveals nothing&quot; would be a slogan rather than a theorem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fiat and Shamir (1986)&lt;/strong&gt; contributed two things in one paper. First, a practical zero-knowledge identification scheme based on factoring. Second, and far more consequential, the &lt;em&gt;transform&lt;/em&gt; that bears their name: replace the verifier&apos;s random challenge with the output of a hash function, and the interactive protocol collapses into a non-interactive one -- a signature [@fiat-shamir]. This is the birth of Move 2, and it arrived as a heuristic, a plausible trick without a proof. That gap between &quot;plausible&quot; and &quot;proven&quot; is where three decades of trouble would later live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schnorr (1989)&lt;/strong&gt; produced the efficient discrete-log archetype we dissected in Section 2: three moves, special soundness by inspection, and -- once you apply Fiat-Shamir -- the compact Schnorr signature.Schnorr&apos;s US Patent 4,995,082, granted in 1991, is widely reported to have expired around 2008. That is why patent-free DSA and later ECDSA colonized the internet through the 1990s, 2000s, and 2010s while Schnorr-proper waited, and why Bitcoin&apos;s BIP-340 could finally standardize native Schnorr signatures only in 2020 [@schnorr-patent], [@bip340]. Part 17 tells that &lt;a href=&quot;https://paragmali.com/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/&quot; rel=&quot;noopener&quot;&gt;signature story&lt;/a&gt;. The irony is total: the scheme with the cleanest security argument was the one the market could not touch, so the industry standardized on messier alternatives and lived with them for twenty years.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pedersen (1991)&lt;/strong&gt; was working on verifiable secret sharing, not commitments, when he handed the field its Move 1 workhorse.The Pedersen commitment debuted as a &lt;em&gt;byproduct&lt;/em&gt; of a non-interactive, information-theoretically secure verifiable secret sharing scheme [@pedersen]. The toolkit&apos;s most-used commitment arrived as a side effect of a paper about something else -- a recurring pattern in this history. His commitment $C = g^m\cdot h^r$ is perfectly hiding, computationally binding, and additively homomorphic [@pedersen] -- three properties we will spend all of Section 4 unpacking, because the entire range-proof line stands on the third one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cramer, Damgard, and Schoenmakers (1994)&lt;/strong&gt; turned Schnorr from a single protocol into a toolkit. Their proofs-of-partial-knowledge construction composes Sigma protocols with AND and OR: prove you know &lt;em&gt;one&lt;/em&gt; of two secrets without revealing which, by honestly answering the branch you know and &lt;em&gt;simulating&lt;/em&gt; the branch you do not [@cds].The name &quot;Sigma protocol&quot; was coined later, in Cramer&apos;s 1996 doctoral thesis. The letter is said to trace the three-move zig-zag of the conversation -- commitment down, challenge across, response back -- though the mnemonic is folklore. The OR-proof trick reuses the very simulator that proves zero knowledge, now as a constructive building block [@cds]. This is the deep reuse the article keeps returning to -- the simulator that proves a single proof leaks nothing is the same object that fakes the clauses you cannot answer in a disjunction. Okamoto&apos;s 1992 identification scheme sharpens the point: it is the witness-indistinguishable sibling of Schnorr, with a two-generator, Pedersen-shaped first move $y = g^{x_1} h^{x_2}$.Because many witness pairs $(x_1, x_2)$ open the same $y = g^{x_1} h^{x_2}$, an Okamoto transcript never reveals &lt;em&gt;which&lt;/em&gt; pair the prover used -- the scheme is witness-indistinguishable by construction [@okamoto]. That is exactly the property a CDS OR-proof leans on to hide which clause is the real one, and it is why the Pedersen shape shows up again the moment you want to hide a witness among many.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pointcheval and Stern (1996)&lt;/strong&gt; finally proved that Fiat-Shamir signatures are unforgeable -- in the random-oracle model -- with the forking lemma: rewind a successful forger, feed it a different challenge on the same commitment, and its two answers extract the discrete log, contradicting hardness [@pointcheval-stern-96], [@pointcheval-stern]. The proof works, but it is &lt;em&gt;loose&lt;/em&gt;: it loses a quadratic factor, and it does not extend cleanly to many sessions running at once. That looseness is not a blemish to be polished away later -- it is the first visible crack of the concurrency failure that Section 6 tears wide open.&lt;/p&gt;

flowchart LR
    A[&quot;1982 Chaum blinding&quot;] --&amp;gt; B[&quot;1985 GMR zero knowledge&quot;]
    B --&amp;gt; C[&quot;1986 Fiat-Shamir transform&quot;]
    C --&amp;gt; D[&quot;1989 Schnorr identification&quot;]
    D --&amp;gt; E[&quot;1991 Pedersen commitment&quot;]
    E --&amp;gt; F[&quot;1994 CDS OR-proofs&quot;]
    F --&amp;gt; G[&quot;1996 forking lemma&quot;]
    G --&amp;gt; H[&quot;2018 Bulletproofs&quot;]
    H --&amp;gt; I[&quot;2021 ROS and MuSig2&quot;]
    I --&amp;gt; J[&quot;2024 FROST RFC 9591&quot;]
    J --&amp;gt; K[&quot;2026 BBS drafts&quot;]
&lt;p&gt;By 1996 the conversation was complete and had a name. The rest of the story is what happened when people started &lt;em&gt;building&lt;/em&gt; with it -- and the failures organize themselves, cleanly, one per move. Start with the move you make first: the commitment.&lt;/p&gt;
&lt;h2&gt;4. Move 1: What You Bind&lt;/h2&gt;
&lt;p&gt;Every proof begins by pinning down a value you are not yet ready to reveal. A commitment is the cryptographic envelope: you seal a value now and open it later, and the sealing must satisfy two opposing demands. It must be &lt;strong&gt;hiding&lt;/strong&gt; -- the sealed envelope leaks nothing about what is inside -- and &lt;strong&gt;binding&lt;/strong&gt; -- you cannot swap the contents after sealing. There are two standard ways to build one, and they sit at opposite corners of a trade-off you cannot escape.&lt;/p&gt;

A protocol to lock in a value now and reveal it later, such that the commitment leaks nothing about the value (hiding) and cannot be opened to a different value than the one committed (binding). No scheme can make both properties perfect at once [@damgard].
&lt;h3&gt;The Pedersen corner&lt;/h3&gt;
&lt;p&gt;Pedersen&apos;s commitment lives in a group of prime order $q$ with two generators $g$ and $h$, where nobody knows the discrete logarithm $\log_g h$. To commit to a message $m$, pick a random blinding scalar $r$ and publish&lt;/p&gt;
&lt;p&gt;$$C = g^m\cdot h^r.$$&lt;/p&gt;
&lt;p&gt;It has three properties, and each one matters downstream [@pedersen]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Perfectly hiding.&lt;/strong&gt; Because $r$ is uniform, $h^r$ is a uniform group element, so $C$ is uniformly distributed regardless of $m$. Even an adversary with unlimited computing power learns &lt;em&gt;nothing&lt;/em&gt; about $m$ from $C$. This is information-theoretic, not merely computational.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Computationally binding.&lt;/strong&gt; Opening $C$ to two different messages means finding $(m, r) \ne (m&apos;, r&apos;)$ with $g^m h^r = g^{m&apos;} h^{r&apos;}$, which rearranges to $\log_g h = (m - m&apos;)/(r&apos; - r)$. In other words, breaking binding &lt;em&gt;is&lt;/em&gt; computing a discrete logarithm. The binding reduction is not near the hard problem; it &lt;em&gt;is&lt;/em&gt; the hard problem [@pedersen].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Additively homomorphic.&lt;/strong&gt; Multiply two commitments and the exponents add: $C(m_1, r_1)\cdot C(m_2, r_2) = C(m_1 + m_2,\ r_1 + r_2)$. You can add committed values without opening them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That third property is the one the whole range-proof line stands on, so hold it in mind. But first, the precision that must never be smoothed over.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A Pedersen commitment is perfectly &lt;em&gt;hiding&lt;/em&gt; and only &lt;em&gt;computationally&lt;/em&gt; binding. Anyone who knows the trapdoor $\log_g h$ can &lt;strong&gt;equivocate&lt;/strong&gt;: open a single commitment to any message they like, by solving one linear equation for a matching blinder. The security of the whole scheme rests on nobody knowing that trapdoor -- which makes the generation of $h$ a security-critical step, not a formality.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Equivocation is the entire Move-1 failure class. If the party who set up the parameters secretly chose $h = g^t$ and kept $t$, then for any commitment $C = g^m h^r$ they can produce a blinder $r&apos;$ that opens it to a different message $m&apos;$ -- because $C = g^{m + t r}$, and they can solve $m&apos; + t r&apos; \equiv m + t r$ for $r&apos;$. The defense is to make the trapdoor un-knowable to everyone.The second generator $h$ must have unknown $\log_g h$, so it is derived by a nothing-up-my-sleeve procedure -- hash a fixed string to a curve point, so that no party could have planted a trapdoor. Part 16 covers &lt;a href=&quot;https://paragmali.com/blog/the-doorway-into-the-group-a-field-guide-to-hashing-a-string/&quot; rel=&quot;noopener&quot;&gt;hashing a string into the group&lt;/a&gt;; the takeaway here is that &quot;pick $h$ honestly&quot; is a real engineering requirement, not a throwaway line.&lt;/p&gt;
&lt;h3&gt;The hash corner&lt;/h3&gt;
&lt;p&gt;The dual construction throws away the algebra. Commit to $m$ by publishing &lt;code&gt;H(m ‖ r)&lt;/code&gt; for a random $r$ and a collision-resistant hash &lt;code&gt;H&lt;/code&gt;. This is computationally hiding and computationally binding -- and its binding rests on collision resistance, not on the discrete logarithm, which makes it the post-quantum-friendly choice. What it gives up is homomorphism: there is no way to add two hash commitments and get a commitment to the sum. Hashes shred structure by design.&lt;/p&gt;
&lt;p&gt;Why can you not have everything -- perfect hiding &lt;em&gt;and&lt;/em&gt; perfect binding? Because it is impossible. If a commitment is perfectly hiding, then $C$ is statistically independent of $m$, which means some opening to every message must exist; a computationally unbounded opener could always equivocate, so binding cannot also be perfect [@damgard]. Pedersen sits at the perfectly-hiding end; hash commitments sit near the perfectly-binding end; nothing sits at both.&lt;/p&gt;

flowchart TD
    Q{&quot;What does the job need?&quot;}
    Q --&amp;gt;|&quot;add or range-prove values&quot;| PED[&quot;Pedersen commitment g to the m times h to the r&quot;]
    Q --&amp;gt;|&quot;post-quantum binding, no algebra&quot;| HASH[&quot;Hash commitment H of m with r&quot;]
    PED --&amp;gt; P1[&quot;perfectly hiding&quot;]
    PED --&amp;gt; P2[&quot;computationally binding on the discrete log&quot;]
    PED --&amp;gt; P3[&quot;additively homomorphic&quot;]
    PED --&amp;gt; P4[&quot;trapdoor holder can equivocate&quot;]
    HASH --&amp;gt; H1[&quot;computationally hiding and binding&quot;]
    HASH --&amp;gt; H2[&quot;not homomorphic&quot;]
    HASH --&amp;gt; H3[&quot;post-quantum from collision resistance&quot;]
&lt;p&gt;Here is the corner choice as a table you can keep next to a design review.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Pedersen &lt;code&gt;g^m · h^r&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Hash &lt;code&gt;H(m ‖ r)&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Hiding&lt;/td&gt;
&lt;td&gt;Perfect (information-theoretic)&lt;/td&gt;
&lt;td&gt;Computational&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binding&lt;/td&gt;
&lt;td&gt;Computational (discrete log)&lt;/td&gt;
&lt;td&gt;Computational (collision resistance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homomorphic&lt;/td&gt;
&lt;td&gt;Yes, additively&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-quantum&lt;/td&gt;
&lt;td&gt;No -- Shor breaks binding&lt;/td&gt;
&lt;td&gt;Yes -- binding survives Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commitment size&lt;/td&gt;
&lt;td&gt;~32 bytes (one ristretto255 point)&lt;/td&gt;
&lt;td&gt;~32 bytes (one SHA-256 output)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equivocation risk&lt;/td&gt;
&lt;td&gt;Trapdoor holder of &lt;code&gt;log_g h&lt;/code&gt; opens to anything&lt;/td&gt;
&lt;td&gt;None -- needs a hash collision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Confidential amounts, range proofs, sums&lt;/td&gt;
&lt;td&gt;PQ binding, timestamps, no algebra needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The homomorphism is not a curiosity; it is the concrete cash-in of Move 1. Committed values that add correctly are exactly what confidential transactions need -- inputs and outputs whose hidden amounts must balance -- and they are the value commitments that a range proof later proves lie in a valid range. Section 7 follows that thread from Pedersen to Bulletproofs. First, watch the homomorphism and the equivocation trapdoor in code.&lt;/p&gt;
&lt;p&gt;{`
const p = 2039n, q = 1019n, g = 4n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function powmod(base, exp, m) {
  let r = 1n; base = mod(base, m);
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = mod(r * base, m); base = mod(base * base, m); exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
// In a real system NOBODY knows t; we plant it here only to demonstrate equivocation.
const t = 503n;                                  // the trapdoor: t = log_g h
const h = powmod(g, t, p);
const commit = (m, r) =&amp;gt; mod(powmod(g, m, p) * powmod(h, r, p), p);&lt;/p&gt;
&lt;p&gt;// 1) Additive homomorphism: C(m1,r1) * C(m2,r2) opens to (m1+m2, r1+r2)
const C1 = commit(100n, 7n), C2 = commit(250n, 9n);
const product = mod(C1 * C2, p);
const summed  = commit(mod(100n + 250n, q), mod(7n + 9n, q));
console.log(&apos;homomorphism holds = &apos; + (product === summed));&lt;/p&gt;
&lt;p&gt;// 2) Equivocation: the trapdoor holder opens ONE commitment to a different message
const m = 100n, r = 7n, C = commit(m, r);
const mFake = 999n;
const rFake = mod(r + (m - mFake) * powmod(t, q - 2n, q), q);   // solve m&apos;+t r&apos; = m+t r
console.log(&apos;same C reopened to &apos; + mFake + &apos; = &apos; + (commit(mFake, rFake) === C));
`}&lt;/p&gt;
&lt;p&gt;A Pedersen commitment binds a value the verifier cannot see. But an interactive proof about it needs a live verifier to send a random challenge -- and live verifiers do not scale to signatures or blockchains. The fix is to fake the verifier with a hash. That single fix is the most-broken move in the toolkit.&lt;/p&gt;
&lt;h2&gt;5. Move 2: What Goes Into the Hash&lt;/h2&gt;
&lt;p&gt;The transform that makes the whole toolkit shippable is one line of code -- and that one line has been written wrong, in production, for thirty years.&lt;/p&gt;
&lt;p&gt;The interactive Sigma protocol needs a live verifier to supply a random challenge $e$. That is fine for a login handshake and useless for a signature or a blockchain, where there is no verifier standing by. Fiat and Shamir&apos;s fix was to &lt;em&gt;derive&lt;/em&gt; the challenge from a hash of the transcript so far: compute $e = H(\dots)$ yourself and proceed [@fiat-shamir]. In the random-oracle model -- where the hash is treated as a truly random function -- the hash &lt;em&gt;is&lt;/em&gt; the verifier, and the interactive proof becomes a non-interactive one.&lt;/p&gt;

A method to make an interactive Sigma protocol non-interactive by replacing the verifier&apos;s random challenge with a hash of the transcript. The prover computes the challenge itself, turning a proof of knowledge into a signature or an on-chain proof [@fiat-shamir].

An idealization that treats a hash function as a truly random function answering consistently to repeated queries. It is the model in which Fiat-Shamir soundness is provable; Part 10 covers the [hash machinery](/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/) it idealizes [@fiat-shamir].
&lt;p&gt;Everything now rides on what you put inside that hash. And this is where the failures cluster, because the answer is not obvious and the wrong answer still runs.&lt;/p&gt;
&lt;h3&gt;The decision rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Strong Fiat-Shamir&lt;/strong&gt; binds everything the proof is about:&lt;/p&gt;
&lt;p&gt;$$e = H(\texttt{domain_separator} ,|, \text{statement} ,|, \text{public params} ,|, a ,|, \text{transcript}).$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weak Fiat-Shamir&lt;/strong&gt; hashes only the commitment, $e = H(a)$, and omits the statement. That omission is the bug. When the challenge does not depend on the statement, a proof built for one statement can be transplanted onto another -- a statement it was never made for -- and it still verifies [@bpw]. The extreme case is a signature: if the message is not in the hash, a signature on &quot;pay 5&quot; is a valid signature on &quot;pay 5000,&quot; because verification never touches the message.&lt;/p&gt;

flowchart TD
    A[&quot;Sigma commitment a and statement x&quot;] --&amp;gt; B{&quot;What enters the challenge hash?&quot;}
    B --&amp;gt;|&quot;weak FS: only a&quot;| W[&quot;e is H of a&quot;]
    B --&amp;gt;|&quot;strong FS: statement, params, a&quot;| S[&quot;e is H of x with params and a&quot;]
    W --&amp;gt; W2[&quot;challenge ignores the statement&quot;]
    W2 --&amp;gt; W3[&quot;transplant the proof onto a false x prime&quot;]
    W3 --&amp;gt; W4[&quot;forgery verifies&quot;]
    S --&amp;gt; S2[&quot;challenge binds the exact statement&quot;]
    S2 --&amp;gt; S3[&quot;transplant recomputes a different e&quot;]
    S3 --&amp;gt; S4[&quot;forgery rejected&quot;]
&lt;p&gt;You can watch the transplant happen and then watch the fix stop it. In the model below, the &quot;statement&quot; is the signed message; weak Fiat-Shamir lets one signature verify against any message, and strong Fiat-Shamir binds it.&lt;/p&gt;
&lt;p&gt;{`
const p = 2039n, q = 1019n, g = 4n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function powmod(base, exp, m) {
  let r = 1n; base = mod(base, m);
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = mod(r * base, m); base = mod(base * base, m); exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
function H(str) { let h = 0n; for (const ch of str) h = mod(h * 131n + BigInt(ch.charCodeAt(0)), q); return mod(h, q); }
const x = 777n, y = powmod(g, x, p);
const r = 421n;   // nonce hardcoded only so the demo is deterministic&lt;/p&gt;
&lt;p&gt;// WEAK Fiat-Shamir: the challenge hashes only the commitment a (message omitted)
const signWeak = () =&amp;gt; { const a = powmod(g, r, p); const e = H(a.toString()); return { a, z: mod(r + e * x, q) }; };
const verifyWeak = (msg, s) =&amp;gt; { const e = H(s.a.toString()); return powmod(g, s.z, p) === mod(s.a * powmod(y, e, p), p); };
const sigW = signWeak();
console.log(&apos;weak  valid on original  = &apos; + verifyWeak(&apos;alice pays 5&apos;, sigW));
console.log(&apos;weak  valid on FORGED    = &apos; + verifyWeak(&apos;alice pays 5000&apos;, sigW));  // true -&amp;gt; universal forgery&lt;/p&gt;
&lt;p&gt;// STRONG Fiat-Shamir: the challenge binds the public key AND the message AND a
const signStrong = (msg) =&amp;gt; { const a = powmod(g, r, p); const e = H(y.toString() + &apos;|&apos; + msg + &apos;|&apos; + a.toString()); return { a, z: mod(r + e * x, q) }; };
const verifyStrong = (msg, s) =&amp;gt; { const e = H(y.toString() + &apos;|&apos; + msg + &apos;|&apos; + s.a.toString()); return powmod(g, s.z, p) === mod(s.a * powmod(y, e, p), p); };
const sigS = signStrong(&apos;alice pays 5&apos;);
console.log(&apos;strong valid on original = &apos; + verifyStrong(&apos;alice pays 5&apos;, sigS));
console.log(&apos;strong valid on FORGED   = &apos; + verifyStrong(&apos;alice pays 5000&apos;, sigS)); // false -&amp;gt; rejected
`}&lt;/p&gt;
&lt;h3&gt;The same bug, forged twice, a decade apart&lt;/h3&gt;
&lt;p&gt;This is not a hypothetical. It is the single most-repeated real-world break in the toolkit, and it has a body count.&lt;/p&gt;
&lt;p&gt;In 2012, Bernhard, Pereira, and Warinschi formalized the weak-versus-strong distinction and showed that the Helios internet-voting system used weak Fiat-Shamir. Its ballot proofs -- the &quot;zero-knowledge&quot; proofs that a vote is well-formed -- could be transplanted and forged [@bpw]. The paper&apos;s title is a warning label: &lt;em&gt;How Not to Prove Yourself&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Ten years later, in April 2022, Trail of Bits disclosed the same class of bug in three live systems at once: Girault&apos;s proof of knowledge, Bulletproofs, and PlonK [@tob-part1], [@tob-girault], [@tob-bp], [@tob-plonk]. The Bulletproofs instance was traced to an insecure Fiat-Shamir generation recommended in the original 2017 paper, and it carried CVE-2022-29566 [@tob-bp], [@cve], [@bulletproofs].Note the attribution carefully: &quot;Girault&quot; is the name of an &lt;em&gt;affected scheme&lt;/em&gt; (a proof of knowledge in an RSA group), not the researcher. The disclosure was by Jim Miller at Trail of Bits [@tob-part1], [@tob-girault].&lt;/p&gt;

&quot;We&apos;ve dubbed this class of vulnerabilities Frozen Heart. The word frozen is an acronym for FoRging Of ZEro kNowledge proofs ... a mistake in the original academic paper, in which the authors recommend an insecure Fiat-Shamir generation.&quot; -- Trail of Bits [@tob-part1]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Helios in 2012 and Frozen Heart in 2022 are the same failure: a challenge hash that omitted the statement. In neither case did anyone break the discrete logarithm. The security lived in one line of the hash, and that line was wrong -- in a shipped e-voting system and in a peer-reviewed range-proof paper -- ten years apart [@bpw], [@tob-part1].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bind the whole statement and the challenge is safe. But Fiat-Shamir also turns the Schnorr &lt;em&gt;identification&lt;/em&gt; protocol into the Schnorr &lt;em&gt;signature&lt;/em&gt; -- and that hands the third move, the response, a fresh way to leak. This one does not even need a mistake in the hash. It needs a mistake in a single random number.&lt;/p&gt;
&lt;h2&gt;6. Move 3: How the Response Behaves&lt;/h2&gt;
&lt;p&gt;The response $z = r + e\cdot x$ is the only message in the whole conversation that touches the secret, and it protects that secret with exactly one thing: a fresh, secret, one-time nonce $r$. Everything the third move can suffer is a story about that nonce -- a number that is never even transmitted, yet is where the key most often escapes.&lt;/p&gt;
&lt;p&gt;Start with the bridge that Fiat-Shamir just built. Apply the transform to Schnorr&apos;s identification protocol and you get the Schnorr signature: the commitment $a$ and response $z$ are the signature, and the challenge is $e = H(y ,|, m ,|, a)$. Read that carefully, because it is one of the article&apos;s quiet revelations: &lt;strong&gt;a Schnorr signature is a non-interactive proof of knowledge of the private key.&lt;/strong&gt; Signing is proving you know $x$; the message rides along in the challenge hash [@bip340].&lt;/p&gt;
&lt;h3&gt;Reuse the nonce, lose the key&lt;/h3&gt;
&lt;p&gt;Now recall the extractor from Section 2. Two accepting transcripts that share the commitment $a$ but differ in the challenge $e$ yield the witness $x = (z_1 - z_2)/(e_1 - e_2)$. That was the &lt;em&gt;defender&apos;s&lt;/em&gt; proof that Schnorr is a proof of knowledge. Turn it around: if a signer produces two signatures with the same nonce $r$, they produce two transcripts with the same $a = g^r$ and different challenges. The attacker runs the same three lines of algebra the extractor runs, and the private key falls out.&lt;/p&gt;
&lt;p&gt;This is the identical failure as a reused ECDSA nonce -- the catastrophe covered in Parts 8 and 17 -- because both signature schemes hide the key behind a per-signature nonce, and both surrender it the moment the nonce repeats.There is a sharp distinction worth carrying. The reused-nonce break in &lt;a href=&quot;https://paragmali.com/blog/one-number-used-twice-how-a-repeated-nonce-hands-over-your-p/&quot; rel=&quot;noopener&quot;&gt;Part 8&lt;/a&gt; happens &lt;em&gt;within&lt;/em&gt; a single scheme when one nonce is used for two signatures. The ROS break below happens &lt;em&gt;across&lt;/em&gt; many concurrent sessions of a multi-party scheme, exploiting the linearity of the response rather than a literal repeat. Same move, two different exploits. A biased nonce -- not repeated, just slightly non-uniform -- leaks the key more slowly, through lattice attacks that aggregate many partial leaks. The discipline is absolute: nonces must be fresh, secret, and uniform, whether drawn from a &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;well-seeded CSPRNG or derived deterministically&lt;/a&gt; as Part 2 describes.&lt;/p&gt;
&lt;h3&gt;Why concurrency is the hard part&lt;/h3&gt;
&lt;p&gt;Pointcheval and Stern&apos;s forking lemma proves single-session Schnorr unforgeable in the random-oracle model: rewind a forger, give it a fresh challenge on the same commitment, and its two answers extract the discrete log, which is assumed hard [@pointcheval-stern]. The proof works, but it pays a price -- a quadratic loss in tightness -- and, more importantly, the rewinding trick does not compose when many sessions run at once. You cannot cleanly rewind one of a hundred interleaved conversations without disturbing the rest.&lt;/p&gt;

The rewinding technique of Pointcheval and Stern that turns a random-oracle forger into a discrete-log solver by running it twice with different challenges on the same commitment. It gives Schnorr and Fiat-Shamir their ROM security proof, but loosely -- with a quadratic security loss -- and only for the single-session setting [@pointcheval-stern].
&lt;p&gt;That gap between &quot;one session is provably secure&quot; and &quot;many concurrent sessions are secure&quot; stayed hidden for two decades. Then it was pried open.&lt;/p&gt;
&lt;h3&gt;The ROS attack&lt;/h3&gt;
&lt;p&gt;In 2021, Benhamouda, Lepoint, Loss, Orru, and Raykova published a polynomial-time solution to the ROS problem, and with it broke naive two-round Schnorr multisignatures and blind Schnorr signatures [@ros]. The mechanism is pure linear algebra. Open enough concurrent signing sessions -- more than about $\log_2 p$ of them -- collect the responses, and set up a linear system whose solution forges a signature on a message the signers never agreed to. No discrete logarithm is solved anywhere in the attack.&lt;/p&gt;

&quot;Random inhomogeneities in an Overdetermined, Solvable system of linear equations.&quot; It is solvable in polynomial time once the number of concurrent sessions exceeds roughly $\log_2 p$, which is exactly what breaks naive linear Schnorr multi-signatures and blind signatures. It is emphatically NOT &quot;Random Oracle Substitution,&quot; a common web mistranslation [@ros].
&lt;p&gt;The 2021 result had a predecessor. In 2019, Drijvers and co-authors had already shown, at IEEE S&amp;amp;P, that the same naive two-round multisignatures were insecure under concurrency -- their attack ran in sub-exponential time using Wagner&apos;s generalized-birthday (k-sum) algorithm [@drijvers], [@wagner]. The 2021 ROS work sharpened sub-exponential into polynomial. Together they closed the case: naive linear Schnorr multisignatures are not merely hard to prove secure under concurrency; they are concretely broken.&lt;/p&gt;

flowchart TD
    Z[&quot;response z holds r plus e times x&quot;] --&amp;gt; B{&quot;how is the nonce misused?&quot;}
    B --&amp;gt;|&quot;reuse or bias one nonce&quot;| R1[&quot;two transcripts share commitment a&quot;]
    R1 --&amp;gt; R2[&quot;extractor recovers x from two responses&quot;]
    B --&amp;gt;|&quot;open many concurrent sessions&quot;| C1[&quot;responses are linear across sessions&quot;]
    C1 --&amp;gt; C2[&quot;ROS solves the linear system&quot;]
    C2 --&amp;gt; C3[&quot;forge a signature, no discrete log solved&quot;]

No discrete log is ever solved. The response simply leaks across concurrent sessions, and linear algebra does the rest.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The simple &quot;everyone sends a nonce, everyone sends a response&quot; two-round multisignature is forgeable under concurrent sessions -- sub-exponentially since Drijvers (S&amp;amp;P 2019), and in polynomial time since ROS (2021). Use MuSig2 (n-of-n) or FROST (t-of-n), which are built specifically to defeat this attack [@drijvers], [@ros].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Move 1 can be equivocated, Move 2 can be transplanted, Move 3 can be solved by linear algebra -- and not one of these touched the discrete logarithm. That is the thesis, proven three times. The story from here is how each sub-lineage climbed &lt;em&gt;out&lt;/em&gt; of its break, while the three moves kept running underneath.&lt;/p&gt;
&lt;h2&gt;7. What Superseded What (and What Never Did)&lt;/h2&gt;
&lt;p&gt;Here is the classic mistake this article exists to correct: there is no single &quot;what superseded what&quot; ladder for this toolkit. The three moves are a &lt;em&gt;substrate&lt;/em&gt;. Supersession happens in the &lt;em&gt;composites&lt;/em&gt; built on top of them, and there are exactly &lt;strong&gt;three construction ladders&lt;/strong&gt; -- one per application: range proofs, multi-party Schnorr, and anonymous credentials. Every rung was forced by a break or a cost on a specific move, never by an advance against the discrete logarithm.&lt;/p&gt;
&lt;p&gt;The one change to the foundation itself -- weak Fiat-Shamir hardened to strong -- is not a fourth ladder but a fix applied in place, so we take it first.&lt;/p&gt;

flowchart TD
    subgraph FOUND[&quot;Foundations, never superseded, Move 2 hardened weak to strong in place&quot;]
        F1[&quot;Move 1 Pedersen&quot;]
        F2[&quot;Move 2 Fiat-Shamir, hardened weak to strong&quot;]
        F3[&quot;Move 3 Schnorr response&quot;]
    end
    FOUND --&amp;gt; RP[&quot;Range proofs&quot;]
    FOUND --&amp;gt; MP[&quot;Multi-party Schnorr&quot;]
    FOUND --&amp;gt; AC[&quot;Anonymous credentials&quot;]
    RP --&amp;gt; RP1[&quot;Borromean 2015&quot;]
    RP1 --&amp;gt;|&quot;superseded by&quot;| RP2[&quot;Bulletproofs 2018&quot;]
    RP2 --&amp;gt;|&quot;superseded by&quot;| RP3[&quot;Bulletproofs plus 2022&quot;]
    MP --&amp;gt; MP1[&quot;naive two-round&quot;]
    MP1 --&amp;gt;|&quot;superseded by&quot;| MP2[&quot;three-round&quot;]
    MP2 --&amp;gt;|&quot;superseded by&quot;| MP3[&quot;MuSig2 and FROST&quot;]
    AC --&amp;gt; AC1[&quot;Chaum blind signature&quot;]
    AC1 --&amp;gt;|&quot;superseded by&quot;| AC2[&quot;CL-Idemix and U-Prove&quot;]
    AC2 --&amp;gt;|&quot;shipped instead&quot;| AC3[&quot;KVAC, BBS, Privacy Pass&quot;]
&lt;h3&gt;Foundation hardening: weak Fiat-Shamir becomes strong&lt;/h3&gt;
&lt;p&gt;The one part of the foundation that changed was never superseded -- it was hardened in place. Weak Fiat-Shamir was the default misreading of the heuristic; Helios in 2012 and Frozen Heart in 2022 forged proofs under it [@bpw], [@tob-part1], [@cve]. Strong Fiat-Shamir -- bind the whole statement -- is what the random-oracle proof had silently assumed all along. The two generations differ only in what enters one hash, and the discrete logarithm is untouched in both. Move 2 was not replaced by a better construction; it was corrected to the instantiation it always needed.&lt;/p&gt;
&lt;h3&gt;Ladder A -- range proofs: cashing in the homomorphism&lt;/h3&gt;
&lt;p&gt;This ladder cashes in Move 1&apos;s homomorphism, and it is the one with dramatic size numbers. The textbook starting point is a bit-decomposition OR-proof: to show a committed value lies in $[0, 2^n)$, prove each of its $n$ bits is either 0 or 1 with an OR-proof. It works and it is $O(n)$ -- linear in the bit width, which is a lot of bytes.&lt;/p&gt;
&lt;p&gt;Monero&apos;s RingCT initially shipped Borromean ring range proofs (Maxwell and Poelstra, 2015), which packed the bit proofs into ring signatures at roughly 6 kB per output -- the dominant cost of a confidential transaction [@monero]. Then &lt;strong&gt;Bulletproofs&lt;/strong&gt; (Bunz, Bootle, Boneh, Poelstra, Wuille, and Maxwell, 2018) reduced the range statement to an inner-product relation and compressed it with a recursive argument that halves the vectors each round, yielding an $O(\log n)$ proof of about 600 to 700 bytes -- with &lt;strong&gt;no trusted setup&lt;/strong&gt; [@bulletproofs], [@monero].&lt;/p&gt;
&lt;p&gt;Monero&apos;s own documentation reports the deployment cut transaction size by at least 80 percent [@monero]. &lt;strong&gt;Bulletproofs+&lt;/strong&gt; (2022) shrank the constant further with a weighted inner-product argument [@bulletproofs-plus].&lt;/p&gt;
&lt;p&gt;The honest footnote on this ladder is the one Section 5 set up: Bulletproofs also &lt;em&gt;broke&lt;/em&gt;, via weak Fiat-Shamir, as CVE-2022-29566 [@tob-bp], [@cve]. That was a Move-2 failure inside a Move-1 construction, and it was fixed by strong Fiat-Shamir -- not by Bulletproofs+. The size ladder and the security fix are independent axes on the same object.&lt;/p&gt;
&lt;h3&gt;Ladder B -- multi-party Schnorr: climbing out of the ROS break&lt;/h3&gt;
&lt;p&gt;This ladder is Move 3 recovering from the concurrency break of Section 6. The naive two-round multisignature -- everyone sums nonces, everyone sums responses -- fell to Drijvers (2019) and then ROS (2021) [@drijvers], [@ros]. The first fix was to spend a round: three-round commit-then-reveal schemes make each signer commit to its nonce before revealing it, which restores concurrent security at the cost of an extra round trip [@musig2].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MuSig2&lt;/strong&gt; (Nick, Ruffing, and Seurin, 2021) got back to two rounds by a clever trick: each signer publishes &lt;em&gt;two&lt;/em&gt; nonces, and the effective nonce is a hash-weighted combination of them, so the combined nonce is a &lt;em&gt;nonlinear&lt;/em&gt; function of the inputs -- exactly what defeats the linear-algebra ROS forgery [@musig2]. &lt;strong&gt;FROST&lt;/strong&gt; (standardized as RFC 9591 in June 2024) then added genuine threshold signing [@rfc9591].&lt;/p&gt;
&lt;p&gt;The distinction that matters: MuSig2, standardized as BIP-327, is &lt;strong&gt;n-of-n&lt;/strong&gt; -- every signer must participate, and the output is a single 64-byte signature indistinguishable from a single-key one [@bip327], [@musig2]. FROST is the &lt;strong&gt;t-of-n threshold&lt;/strong&gt; scheme, where any $t$ of $n$ signers suffice [@rfc9591]. They are different tools for different policies, not two names for the same thing.&lt;/p&gt;
&lt;h3&gt;Ladder C -- anonymous credentials: the elegant generation that lost&lt;/h3&gt;
&lt;p&gt;This ladder is the strange one, because the most elegant generation is the one that did &lt;em&gt;not&lt;/em&gt; win. It begins with Chaum&apos;s blind signatures -- blind, let the issuer sign, unblind -- introduced in 1982 [@chaum-blind] and set out as a whole transaction system in his 1985 paper, the root of unlinkable credentials, though single-show [@chaum-cacm]. It climbs to the multi-attribute systems: Camenisch-Lysyanskaya (Idemix) and Brands (U-Prove), which let a holder prove statements about many attributes and were genuinely beautiful cryptography [@cl-2001], [@cl-2004], [@uprove]. And then, mostly, it did not ship.&lt;/p&gt;

Camenisch-Lysyanskaya / Idemix and Brands / U-Prove were the theoretically complete multi-attribute credentials of the 2000s -- standardized (U-Prove under ISO/IEC 18370-2 and released under Microsoft&apos;s Open Specification Promise) and mathematically lovely [@uprove], [@cl-2001], [@cl-2004]. Yet they were largely never deployed at population scale, losing on cost, complexity, and integration friction. The systems that shipped instead were pragmatic ones -- Signal&apos;s keyed-verification credentials, BBS, and Privacy Pass. This is the rare case in cryptography where the most elegant design is not the one that wins -- a deployment failure, not a cryptographic break. The exception proves the rule: Direct Anonymous Attestation, a Camenisch-Lysyanskaya-family credential presented through a Sigma proof of possession, shipped in billions of TPM security chips -- precisely because its target was fixed hardware with a captive issuer, exactly the friction the general-purpose systems could not overcome [@daa], [@tpm2-spec].
&lt;p&gt;The winners are three deployed corners, not one. &lt;strong&gt;Signal KVAC&lt;/strong&gt; (2020) is a keyed-verification credential where the issuer is also the verifier, built on an algebraic MAC and a Sigma-protocol presentation proof, deployed in Signal&apos;s private groups [@signal], [@kvac]. &lt;strong&gt;BBS&lt;/strong&gt; signatures are the publicly-verifiable corner, a multi-message credential now in IETF drafts [@bbs-draft].&lt;/p&gt;
&lt;p&gt;And &lt;strong&gt;Privacy Pass / Apple Private Access Tokens&lt;/strong&gt;, standardized at the IETF in June 2024, are the unlinkable single-use authorization-token corner -- the deployed realization of Chaum&apos;s blind-token idea, in which a client redeems an anonymously issued token at an origin that cannot link it back to issuance [@rfc9576], [@cloudflare-pat]. All three are Sigma proofs over the same three moves, filled in for three different trust models; Section 8 attaches the parameters to each.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every real-world break lives on one of the three moves. The discrete logarithm was never broken. The commitment equivocates, the challenge transplants, the response leaks -- and the composites climb their ladders while Pedersen, Fiat-Shamir, and Schnorr keep running underneath, never superseded.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the whole picture as one matrix.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Lineage / move&lt;/th&gt;
&lt;th&gt;Key idea&lt;/th&gt;
&lt;th&gt;Size or cost&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Weak Fiat-Shamir&lt;/td&gt;
&lt;td&gt;1986&lt;/td&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;hash only the commitment&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Superseded (insecure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong Fiat-Shamir&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;bind the whole statement&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bit-decomposition range proof&lt;/td&gt;
&lt;td&gt;classic&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;OR-prove each bit&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Historical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Borromean range proof&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;ring signatures over bits&lt;/td&gt;
&lt;td&gt;~6 kB/output&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;recursive inner-product&lt;/td&gt;
&lt;td&gt;~600-700 B, O(log n)&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs+&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;weighted inner-product&lt;/td&gt;
&lt;td&gt;shorter constant&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive two-round multisig&lt;/td&gt;
&lt;td&gt;2010s&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;sum nonces and responses&lt;/td&gt;
&lt;td&gt;2 rounds, broken&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-round multisig&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;commit-then-reveal nonces&lt;/td&gt;
&lt;td&gt;3 rounds&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuSig2 (BIP-327)&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;two nonces, nonlinear combine&lt;/td&gt;
&lt;td&gt;2 rounds, n-of-n&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROST (RFC 9591)&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;threshold nonce commitments&lt;/td&gt;
&lt;td&gt;2 rounds, t-of-n&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chaum blind signature&lt;/td&gt;
&lt;td&gt;1982&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;blind, sign, unblind&lt;/td&gt;
&lt;td&gt;single-show&lt;/td&gt;
&lt;td&gt;Historical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CL-Idemix / U-Prove&lt;/td&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;multi-attribute proofs&lt;/td&gt;
&lt;td&gt;multi-show (Idemix); single-show (U-Prove)&lt;/td&gt;
&lt;td&gt;Niche (undeployed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal KVAC&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;algebraic MAC, issuer verifies&lt;/td&gt;
&lt;td&gt;deployed&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BBS&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;pairing, multi-message&lt;/td&gt;
&lt;td&gt;~80 B signature&lt;/td&gt;
&lt;td&gt;Standardizing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Pass / Apple PAT&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;unlinkable single-use token&lt;/td&gt;
&lt;td&gt;RFC 9576/9577/9578&lt;/td&gt;
&lt;td&gt;Deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Three ladders, three break-driven climbs, over a foundation that was itself hardened in place -- three ascents on one unchanging three-move base. Which rung is at the top of each ladder today -- and with which exact parameters -- is the difference between a design review that passes and one that ships a CVE.&lt;/p&gt;
&lt;h2&gt;8. State of the Art (2024-2026): What Actually Ships&lt;/h2&gt;
&lt;p&gt;The frontier is not one best algorithm. It is a layered stack: three foundational moves that were never superseded, and above them a handful of composites that ship at scale, each with a standard and a parameter set. If you have to build with this in 2026, here is the state of the art with the numbers attached.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The moves, still the state of the art as primitives.&lt;/strong&gt; A Pedersen commitment is a single 32-byte point on ristretto255. Strong Fiat-Shamir is the correct instantiation now baked into every standard below. And Schnorr-proper, standardized as &lt;strong&gt;BIP-340&lt;/strong&gt;, is a 64-byte signature [@bip340]; Bitcoin&apos;s Taproot upgrade activated it at block 709,632 in November 2021 [@bip341], [@taproot-wiki], and native Schnorr has been available for every spend since. These are not legacy pieces waiting to be replaced; they are the load-bearing base of everything else.&lt;/p&gt;

BIP-340 is Schnorr-proper over the secp256k1 curve [@bip340]. EdDSA and its instance Ed25519 are also Schnorr-*family* signatures -- same linear response structure -- but with different encoding, different nonce derivation, and a different curve. They are cousins, not the same standard, and conflating them causes real interoperability and security-review confusion. When someone says &quot;Schnorr signature,&quot; ask which one.
&lt;p&gt;&lt;strong&gt;MuSig2 / BIP-327&lt;/strong&gt; is the deployed answer for n-of-n multisignatures: a two-round protocol producing a single 64-byte Schnorr signature that is indistinguishable on-chain from a single-key spend, implemented in &lt;code&gt;libsecp256k1&lt;/code&gt; and &lt;code&gt;secp256k1-zkp&lt;/code&gt; [@bip327], [@musig2]. Every co-signer participates, and the verifier does one ordinary Schnorr check.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FROST / RFC 9591&lt;/strong&gt; (June 2024) is the most consequential &lt;em&gt;new&lt;/em&gt; standard in the window: a two-round t-of-n threshold Schnorr signature, specified across five ciphersuites -- Ed25519, ristretto255, Ed448, P-256, and secp256k1 [@rfc9591]. On its own, FROST assumes signers do not vanish mid-protocol; &lt;strong&gt;ROAST&lt;/strong&gt; (CCS 2022) wraps it to keep signing alive even when some signers are faulty or offline, giving fault tolerance for asynchronous deployments [@roast].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Nonce structure&lt;/th&gt;
&lt;th&gt;Rounds&lt;/th&gt;
&lt;th&gt;Concurrent-secure&lt;/th&gt;
&lt;th&gt;Fault-tolerant&lt;/th&gt;
&lt;th&gt;Signature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Naive two-round&lt;/td&gt;
&lt;td&gt;sum the nonces&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;No (Drijvers, ROS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;Broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-round&lt;/td&gt;
&lt;td&gt;commit then reveal&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuSig2 (BIP-327)&lt;/td&gt;
&lt;td&gt;two nonces, nonlinear&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B (n-of-n)&lt;/td&gt;
&lt;td&gt;Deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROST (RFC 9591)&lt;/td&gt;
&lt;td&gt;threshold commitments&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B (t-of-n)&lt;/td&gt;
&lt;td&gt;Standardized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROST + ROAST&lt;/td&gt;
&lt;td&gt;ROAST wrapper&lt;/td&gt;
&lt;td&gt;2+&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;64 B (t-of-n)&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

A zero-knowledge proof that a committed value lies in a fixed interval such as $[0, 2^n)$ without revealing the value. Bulletproofs are the deployed transparent construction, proving a Pedersen-committed amount is in range in about 600 to 700 bytes [@bulletproofs].
&lt;p&gt;&lt;strong&gt;Bulletproofs and Bulletproofs+&lt;/strong&gt; are the state of the art for &lt;em&gt;transparent&lt;/em&gt; range proofs -- logarithmic size, no trusted setup -- and they secure confidential amounts in Monero, where Bulletproofs deployed in October 2018 and Bulletproofs+ followed in 2022 [@bulletproofs], [@bulletproofs-plus], [@monero]. They are the default whenever you need a range proof without a setup ceremony and can accept linear verification time. This is the immediate cash-in of Move 1&apos;s homomorphism at production scale.&lt;/p&gt;
&lt;p&gt;On the credential side, three systems ship, and they occupy different economic corners.&lt;/p&gt;

An anonymous credential in which the issuer and the verifier are the same party, so the credential can be built on a fast algebraic MAC instead of a public-key signature. The holder presents it with a Sigma-protocol proof of knowledge, revealing only the attributes it chooses [@kvac], [@signal].
&lt;p&gt;&lt;strong&gt;Signal KVAC&lt;/strong&gt; is the keyed-verification credential in production in Signal&apos;s private group system: the issuer is also the verifier, so it runs on an algebraic MAC with no pairings, and presentation is a Sigma-protocol proof of knowledge [@kvac], [@signal]. It is the standing counter-example to &quot;elegant but undeployed.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BBS and Blind BBS&lt;/strong&gt;BBS takes its name from the 2004 Boneh-Boyen-Shacham short group signature scheme it descends from, by way of the later BBS+ variant [@bbs-origin]. are the publicly-verifiable answer: multi-message, selective-disclosure credentials over the BLS12-381 pairing curve, with roughly 80-byte signatures, now in &lt;code&gt;draft-irtf-cfrg-bbs-signatures-10&lt;/code&gt; (8 January 2026) and Blind BBS &lt;code&gt;draft-03&lt;/code&gt; [@bbs-draft], [@blind-bbs]. BBS is the standardizing choice for W3C Verifiable Credentials and identity wallets, where a third party must verify what an issuer signed. KVAC and BBS are parallel answers -- private-issuer economy versus public verifier -- not one replacing the other.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Privacy Pass&lt;/strong&gt;, deployed at population scale as &lt;strong&gt;Apple Private Access Tokens&lt;/strong&gt;, is the third corner: a different shape from the other two -- an unlinkable single-use authorization token rather than a multi-attribute credential. A client anonymously obtains a token backed by some attestation and later redeems it at an origin that cannot link the redemption to issuance.&lt;/p&gt;
&lt;p&gt;It was standardized at the IETF in June 2024 across three documents -- &lt;strong&gt;RFC 9576&lt;/strong&gt; for the architecture, &lt;strong&gt;RFC 9578&lt;/strong&gt; for issuance, and &lt;strong&gt;RFC 9577&lt;/strong&gt; for the &lt;code&gt;PrivateToken&lt;/code&gt; HTTP authentication scheme [@rfc9576], [@rfc9578], [@rfc9577]. Apple shipped it in iOS 16 and macOS Ventura in 2022 with hardware-backed device attestation, and Cloudflare uses it to drop the CAPTCHA for attested devices [@cloudflare-pat].Direct Anonymous Attestation (2004), the deployed exception detailed in Section 7, ships in two hardware generations -- TPM 1.2 RSA-DAA and TPM 2.0 ECDAA -- and adds per-verifier basename linkability, so a device proves it is genuine without becoming trackable [@daa], [@tpm2-spec].&lt;/p&gt;
&lt;p&gt;Underneath, it is still commit, challenge, respond over the discrete log. Keep it distinct from KVAC and BBS: it is single-&lt;em&gt;show&lt;/em&gt; authorization, not multi-attribute selective disclosure, and its privately-verifiable issuance is a verifiable oblivious PRF whose Chaum-Pedersen mechanism Part 20 owns and this section only signposts.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Verifiability&lt;/th&gt;
&lt;th&gt;Cryptographic base&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;U-Prove / Idemix&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;Discrete log, RSA, pairings&lt;/td&gt;
&lt;td&gt;multi-kB&lt;/td&gt;
&lt;td&gt;Standardized, largely undeployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal KVAC&lt;/td&gt;
&lt;td&gt;Keyed (issuer verifies)&lt;/td&gt;
&lt;td&gt;Algebraic MAC, no pairings&lt;/td&gt;
&lt;td&gt;Compact&lt;/td&gt;
&lt;td&gt;Deployed in Signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BBS / Blind BBS&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;Pairings on BLS12-381&lt;/td&gt;
&lt;td&gt;~80 B signature&lt;/td&gt;
&lt;td&gt;Standardizing (IETF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Pass / Apple PAT&lt;/td&gt;
&lt;td&gt;Public or keyed, per token type&lt;/td&gt;
&lt;td&gt;VOPRF or blind RSA issuance&lt;/td&gt;
&lt;td&gt;Small single-use token&lt;/td&gt;
&lt;td&gt;Deployed (iOS 16, macOS Ventura)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is the same primitive Part 20 built on: an &lt;a href=&quot;https://paragmali.com/blog/the-server-helped-and-never-saw-a-field-guide-to-oblivious-p/&quot; rel=&quot;noopener&quot;&gt;oblivious PRF&apos;s verifiability&lt;/a&gt; rests on a Chaum-Pedersen Sigma proof -- and Privacy Pass&apos;s privately-verifiable issuance is exactly that VOPRF. The deployed &lt;a href=&quot;https://paragmali.com/blog/the-age-gate-that-doesnt-know-your-age-how-anonymous-credent/&quot; rel=&quot;noopener&quot;&gt;anonymous-credential systems&lt;/a&gt; are Sigma proofs of knowledge of a signature, and &lt;a href=&quot;https://paragmali.com/blog/direct-anonymous-attestation-the-zero-knowledge-proof-alread/&quot; rel=&quot;noopener&quot;&gt;Direct Anonymous Attestation&lt;/a&gt; is the Sigma-protocol credential already shipping in TPM silicon. Every composite in this section is the three moves with the parameters filled in.&lt;/p&gt;
&lt;p&gt;Every one of these is a defensible point on a Pareto frontier, not a single winner -- which is exactly why the toolkit is a toolkit. To choose between them, and between the whole family and a general-purpose proof system, you need the line where Sigma plus Fiat-Shamir stops being the right tool.&lt;/p&gt;
&lt;h2&gt;9. Competing Approaches and Where the Line Is&lt;/h2&gt;
&lt;p&gt;The toolkit&apos;s real competition is the general-purpose proof system -- the SNARK or STARK that can prove &lt;em&gt;any&lt;/em&gt; circuit. Knowing when &lt;em&gt;not&lt;/em&gt; to reach for one is half of using this toolkit well.&lt;/p&gt;
&lt;p&gt;A bespoke Sigma proof plus strong Fiat-Shamir is transparent (it needs only the random-oracle assumption, no setup ceremony), tiny for narrow statements (a constant number of group elements, growing only with the number of clauses), and cheap to prove. A pairing-based SNARK gives a constant-size proof and near-constant verification, but at the price of a trusted setup: Groth16 lands near 200 bytes and PLONK near 400 [@groth16], [@plonk].&lt;/p&gt;
&lt;p&gt;A STARK is transparent and plausibly post-quantum, but its proofs run to tens or hundreds of kilobytes. The Bulletproofs authors made the transparency contrast explicit: their range proofs need no trusted setup, unlike the succinct pairing SNARKs [@bulletproofs].&lt;/p&gt;
&lt;p&gt;The decision rule falls out of the shape of your statement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;fixed algebraic relation&lt;/strong&gt; -- &quot;I know a discrete log,&quot; &quot;this Pedersen value is in range,&quot; &quot;one of these $n$ public keys signed&quot; -- wants a hand-verified Sigma proof with strong Fiat-Shamir. It is smaller, faster, and setup-free.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;arbitrary circuit&lt;/strong&gt; -- &quot;this value is the output of running this program&quot; -- is where you reach for a SNARK or STARK, accepting the setup or the proof size in exchange for expressiveness you cannot get from a fixed relation.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Proof size (64-bit range)&lt;/th&gt;
&lt;th&gt;Trusted setup&lt;/th&gt;
&lt;th&gt;Prover&lt;/th&gt;
&lt;th&gt;Verify&lt;/th&gt;
&lt;th&gt;Post-quantum&lt;/th&gt;
&lt;th&gt;Aggregation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Bit-decomposition OR&lt;/td&gt;
&lt;td&gt;O(n), large&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Borromean&lt;/td&gt;
&lt;td&gt;~6 kB/output&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs&lt;/td&gt;
&lt;td&gt;~0.6-0.7 kB, O(log n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n), batchable&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs+&lt;/td&gt;
&lt;td&gt;smaller constant, O(log n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n), batchable&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNARK (Groth16/PLONK)&lt;/td&gt;
&lt;td&gt;~0.2 kB (Groth16) to ~0.4 kB (PLONK), O(1)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;O(circuit)&lt;/td&gt;
&lt;td&gt;~O(1)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Circuit-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STARK&lt;/td&gt;
&lt;td&gt;tens to hundreds of kB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;polylog&lt;/td&gt;
&lt;td&gt;Plausibly&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two more axes decide the corner. &lt;strong&gt;Commitment choice&lt;/strong&gt; is the Section 4 recap: need to add committed values, pick Pedersen; need post-quantum binding or no algebra, pick a hash commitment. And &lt;strong&gt;interactivity&lt;/strong&gt; is a genuine fork: an interactive Sigma protocol is &lt;em&gt;deniable&lt;/em&gt; -- because anyone could have simulated the transcript, it proves nothing to a third party -- and needs no random-oracle assumption, while Fiat-Shamir buys you a signature or an on-chain proof at the cost of making the proof transferable to everyone forever. Deniability versus non-interactivity is a real tension we return to in the open problems.&lt;/p&gt;

The challenge move is the shared weak point of proof systems well beyond this toolkit. PlonK -- a general-purpose SNARK, not a Sigma protocol -- was hit by the *same* weak Fiat-Shamir bug as Bulletproofs in the 2022 Frozen Heart disclosure [@tob-plonk], [@tob-part1]. And in 2025, Khovratovich, Rothblum, and Soukhanov demonstrated a *practical* soundness break against a GKR-based commercial proof system, tricking it into certifying false statements regardless of which hash function instantiates Fiat-Shamir [@khovratovich], [@quanta]. This boundary keeps the article honest: weak-challenge failures are not unique to Sigma protocols -- the move that most often leaks is the same across the whole family of proof systems.
&lt;p&gt;The line is drawn by the shape of your statement and the trust you can assume. But even the right tool, used perfectly, runs into limits that no parameter choice can buy its way out of.&lt;/p&gt;
&lt;h2&gt;10. Theoretical Limits&lt;/h2&gt;
&lt;p&gt;The discrete log has held for forty years. But &quot;the hard problem is hard&quot; is not the same claim as &quot;your proof is secure,&quot; and the gap between them is where this section lives. Every limit below survives even if the discrete logarithm stays hard forever.&lt;/p&gt;
&lt;p&gt;Start with the good news, because it is genuinely good. For the &lt;em&gt;interactive&lt;/em&gt; protocol, special soundness gives a tight, unconditional extractor -- two transcripts and three lines of algebra recover the witness, no assumptions -- and SHVZK gives a &lt;em&gt;perfect&lt;/em&gt; simulator [@damgard]. Interactive Schnorr is about as close to an ideal proof of knowledge as anything in cryptography gets. The trouble starts when you make it non-interactive, or run many copies at once.&lt;/p&gt;
&lt;h3&gt;Soundness is only ROM-deep&lt;/h3&gt;
&lt;p&gt;Fiat-Shamir soundness is provable, but only in the random-oracle model, via the forking lemma [@pointcheval-stern]. In the standard model it is &lt;em&gt;uninstantiable&lt;/em&gt;: Goldwasser and Kalai (FOCS 2003) exhibited schemes whose Fiat-Shamir transform is insecure for &lt;em&gt;every&lt;/em&gt; concrete hash function you could plug in [@gk-focs]. That was a theoretical construction for two decades. Then in 2025, Khovratovich, Rothblum, and Soukhanov turned it practical, making a GKR-based commercial proof system accept false statements regardless of the hash instantiating the transform [@khovratovich], [@quanta].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Khovratovich-Rothblum-Soukhanov result targets Fiat-Shamir applied to GKR-style succinct arguments, not the short algebraic Sigma statements that strong Fiat-Shamir fully binds [@khovratovich]. It does not forge a Schnorr signature. What it does is harden the general lesson: Fiat-Shamir soundness is a property of the random-oracle idealization, and the more your proof leans on the hash doing something a real hash may not do, the thinner the ice [@gk-focs].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Forking is loose, and looseness is the seam&lt;/h3&gt;
&lt;p&gt;The random-oracle reduction is not tight. Rewinding a forger to extract the discrete log loses roughly a quadratic factor in the security bound [@pointcheval-stern]. This is not sloppy proof engineering; it is inherent to the rewinding technique.The quadratic looseness and the concurrency failure are the same fact seen twice. Rewinding extracts from &lt;em&gt;one&lt;/em&gt; session by replaying it with a fresh challenge; it does not compose when many sessions interleave, because you cannot rewind one without disturbing the others. That is precisely the seam the ROS attack pries open [@pointcheval-stern], [@ros]. One line of work sidesteps rewinding entirely: the Fischlin transform (2005) uses an &lt;em&gt;online&lt;/em&gt;, straight-line extractor that reads the witness out of a single transcript by inspecting the prover&apos;s oracle queries, so its extraction composes concurrently and carries more cleanly into the quantum random-oracle model -- at the price of larger proofs, which is why it coexists with Fiat-Shamir rather than replacing it [@fischlin]. And because rewinding does not compose across concurrent sessions, the single-session forking proof simply does not extend to the multi-party concurrent setting.&lt;/p&gt;
&lt;p&gt;That gap has a sharp quantitative edge. Naive linear Schnorr multisignatures and blind signatures are secure only while the number of concurrent open sessions stays below roughly $\log_2 p$; above that threshold, the ROS forgery runs in polynomial time with no discrete log solved [@ros]. MuSig2&apos;s two-nonce nonlinear combination is the construction that evades the bound -- not a discrete-log advance, just a change that makes the response nonlinear so the linear system has no solution [@musig2].&lt;/p&gt;
&lt;h3&gt;Two settled negatives&lt;/h3&gt;
&lt;p&gt;Some limits are not open questions; they are proven impossibilities you must design around.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No commitment is both perfectly hiding and perfectly binding.&lt;/strong&gt; The Move-1 trade-off of Section 4 is fundamental, not an artifact of Pedersen [@damgard]. You choose a corner; you do not get both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;None of this is post-quantum.&lt;/strong&gt; Every guarantee in the article rests on the hardness of the discrete logarithm or of pairing problems, and Shor&apos;s algorithm breaks all of them on a sufficiently large quantum computer. No parameter choice rescues it -- a bigger group buys nothing against Shor. The hard problem that held against classical attackers for forty years is exactly the one a quantum computer dissolves.&lt;/li&gt;
&lt;/ol&gt;

The discrete log is hard, and this multisignature is secure under concurrency, are simply different claims -- and the second one was false until MuSig2 made the combined nonce nonlinear.
&lt;p&gt;Those are the settled negatives. The open frontiers -- where the field is still moving -- are next, and the sharpest is the quantum cliff the whole toolkit stands at the edge of.&lt;/p&gt;
&lt;h2&gt;11. Open Problems&lt;/h2&gt;
&lt;p&gt;The toolkit is deployed and load-bearing, and it still has open edges sharp enough to end careers on. Here is what is genuinely unsolved, and which of it should keep a protocol designer awake.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standard-model Fiat-Shamir.&lt;/strong&gt; A universal, hash-instantiable, tight Fiat-Shamir is believed impossible in general. The 2003 uninstantiability result and the 2025 practical break together bound the ambition: the realistic goal is not a universal fix but per-protocol &lt;em&gt;correlation intractability&lt;/em&gt; -- proving a specific hash is good enough for a specific proof system [@gk-focs], [@khovratovich]. Straight-line alternatives such as the Fischlin transform trade larger proofs for cleaner, rewinding-free extraction, but they are not a universal drop-in replacement [@fischlin]. For the short algebraic statements in this article, strong Fiat-Shamir remains the right practice; for succinct arguments, the question is live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-quantum Sigma protocols.&lt;/strong&gt; This is the sharpest frontier, because it is where the whole toolkit meets its deadline. Discrete-log zero knowledge dies to Shor -- but the three-move &lt;em&gt;template&lt;/em&gt; does not. It reappears over other hardness assumptions.The Sigma template outlives its discrete-log instantiation. Fiat-Shamir-with-aborts over lattices is the shape behind Dilithium-style signatures, and MPC-in-the-head -- instantiated as Picnic -- builds a zero-knowledge signature from a hash and a secret-sharing protocol, needing no structured hardness assumption at all [@picnic]. Commit, challenge, respond survives the move to a post-quantum world; only the algebra underneath changes. What does &lt;em&gt;not&lt;/em&gt; yet transfer cleanly is the efficient, verifiable, partially-oblivious machinery: post-quantum range proofs and post-quantum multi-show credentials exist but are large and immature compared to Bulletproofs and BBS. The &lt;a href=&quot;https://paragmali.com/blog/the-thirty-year-migration-ships-in-a-pip-install-how-post-qu/&quot; rel=&quot;noopener&quot;&gt;thirty-year migration to post-quantum cryptography&lt;/a&gt; is the sibling part that tracks this deadline in full.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Round-optimal blind Schnorr after ROS.&lt;/strong&gt; Plain two-round blind Schnorr is inherently ROS-vulnerable [@ros]. Getting a secure blind signature in few rounds needs extra structure: clause or boosting variants that break the linearity, a proof in the algebraic group model (Fuchsbauer, Plouviez, and Seurin), or a different scheme entirely [@fps]. There is no known plain, standard-model, round-optimal blind Schnorr that survives concurrency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Threshold Schnorr, hardened.&lt;/strong&gt; RFC 9591 standardized two-round threshold Schnorr for custody in June 2024, and FROST is now entering production custody [@rfc9591], but stronger guarantees remain research targets: adaptive security (against an attacker who corrupts signers mid-protocol), proofs that do not lean on the algebraic group model, and distributed key generation that scales asynchronously to large signer sets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anonymous-credential revocation at population scale&lt;/strong&gt; without breaking unlinkability is still mostly finessed rather than solved -- deployed systems lean on short-lived credentials that simply expire, because a true privacy-preserving revocation list that scales to millions of holders is hard to make both efficient and unlinkable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deniability versus non-interactivity.&lt;/strong&gt; A Fiat-Shamir&apos;d proof is transferable -- anyone who receives it is convinced, forever. An interactive Sigma proof is deniable -- because it could have been simulated, it convinces only the live verifier. Getting both properties at once is a per-application tension, not a solved problem.&lt;/p&gt;
&lt;p&gt;None of these block you from shipping today; they tell you what to watch. What you ship comes down to a small, unglamorous set of rules that separate a correct deployment from a forged one.&lt;/p&gt;
&lt;h2&gt;12. Use X With These Parameters In Case Y&lt;/h2&gt;
&lt;p&gt;Here are the rules a reviewer can apply without re-deriving anything, organized -- of course -- by the three moves, in the order you make them.&lt;/p&gt;
&lt;h3&gt;Move 1: pick the commitment by the job&lt;/h3&gt;
&lt;p&gt;Need to add or range-prove committed values -- confidential amounts, homomorphic tallies? Use a &lt;strong&gt;Pedersen commitment on ristretto255&lt;/strong&gt;, and derive the second generator $h$ by hashing a fixed string to a curve point so that nobody holds the trapdoor $\log_g h$. A known trapdoor is silent equivocation; the generator setup is security-critical, not boilerplate. Need post-quantum binding, or you are committing to something with no algebra -- a Merkle leaf, a coin flip? Use a &lt;strong&gt;hash commitment&lt;/strong&gt; &lt;code&gt;H(m ‖ r)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Move 2: get the challenge right, every time&lt;/h3&gt;
&lt;p&gt;Use &lt;strong&gt;strong Fiat-Shamir&lt;/strong&gt;: hash the full statement, all public parameters, the prior transcript, and a domain separator. The operational habit that prevents the entire Frozen Heart class is one sentence: for every proof, write down &quot;what is this proof &lt;em&gt;about&lt;/em&gt;?&quot; and confirm each item on that list is inside the hash. The statement is about a public key? The public key goes in the hash. It is about a specific message or ciphertext? That goes in too. Weak Fiat-Shamir is what happens when that list is incomplete [@tob-part1].&lt;/p&gt;
&lt;h3&gt;Move 3: nonce discipline&lt;/h3&gt;
&lt;p&gt;Nonces must be fresh, secret, and uniform per session. Draw them from a well-seeded CSPRNG, or derive them deterministically with the tagged-hash discipline BIP-340 specifies so a broken RNG cannot repeat or bias them [@bip340]. In MuSig2 and FROST, honor the two-nonce and preprocessing rules exactly, and never reuse a nonce commitment across sessions -- the multi-party schemes defeat ROS only if the nonce rules are followed [@musig2], [@rfc9591].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Move 1 -- is your second generator $h$ from a nothing-up-my-sleeve hash, with no known trapdoor? Move 2 -- write down what the proof is about, and verify every item is inside the challenge hash, behind a domain separator. Move 3 -- is every nonce fresh, secret, and uniform, and are the MuSig2 or FROST nonce rules followed to the letter? Three questions, in order. Most Frozen-Heart and nonce-reuse incidents fail one of them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Then pick the composite&lt;/h3&gt;

flowchart TD
    START{&quot;What are you building?&quot;}
    START --&amp;gt;|&quot;commit to a value&quot;| COMMIT{&quot;need to add or range-prove it?&quot;}
    COMMIT --&amp;gt;|&quot;yes&quot;| C1[&quot;Pedersen on ristretto255&quot;]
    COMMIT --&amp;gt;|&quot;no, need PQ binding&quot;| C2[&quot;Hash commitment&quot;]
    START --&amp;gt;|&quot;prove a range&quot;| C3[&quot;Bulletproofs plus, aggregated, strong FS&quot;]
    START --&amp;gt;|&quot;multi-party signature&quot;| SIG{&quot;all signers or t of n?&quot;}
    SIG --&amp;gt;|&quot;all n&quot;| C4[&quot;MuSig2 BIP-327&quot;]
    SIG --&amp;gt;|&quot;t of n&quot;| C5[&quot;FROST RFC 9591 plus ROAST&quot;]
    START --&amp;gt;|&quot;anonymous credential&quot;| CRED{&quot;who verifies?&quot;}
    CRED --&amp;gt;|&quot;the issuer itself&quot;| C6[&quot;Signal KVAC&quot;]
    CRED --&amp;gt;|&quot;any third party&quot;| C7[&quot;BBS&quot;]
    START --&amp;gt;|&quot;arbitrary circuit&quot;| C8[&quot;SNARK or STARK&quot;]
&lt;p&gt;The composite rules, stated plainly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Range proof, small and transparent:&lt;/strong&gt; Bulletproofs+, aggregated, with strong Fiat-Shamir binding [@bulletproofs-plus].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All parties co-sign one key (n-of-n):&lt;/strong&gt; MuSig2, standardized as BIP-327 [@bip327].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Any t of n signers suffice:&lt;/strong&gt; FROST, RFC 9591, run with a distributed key generation -- and add ROAST if a faulty signer must not be able to stall the protocol [@rfc9591], [@roast].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authenticate your own users anonymously (you issue and verify):&lt;/strong&gt; Signal KVAC [@kvac], [@signal].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A third party must verify selective disclosure:&lt;/strong&gt; BBS [@bbs-draft].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A fixed algebraic relation, tiny and setup-free:&lt;/strong&gt; a hand-rolled Sigma proof with strong Fiat-Shamir, using a vetted library -- never your own group arithmetic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An arbitrary circuit:&lt;/strong&gt; a SNARK or STARK, accepting the setup or proof-size cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anything that must stay secure through the quantum transition:&lt;/strong&gt; do not rely on these guarantees; plan a post-quantum migration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the implementations, use the libraries that encode these defaults: &lt;code&gt;bitcoin-core/secp256k1&lt;/code&gt; and &lt;code&gt;secp256k1-zkp&lt;/code&gt; for BIP-340 and MuSig2 [@bip340], [@bip327]; FROST implementations validated against the RFC 9591 test vectors [@rfc9591]; &lt;code&gt;dalek-cryptography&lt;/code&gt; for Bulletproofs; Signal&apos;s &lt;code&gt;libsignal&lt;/code&gt; and zkgroup for KVAC [@signal]; BBS libraries tracking the CFRG drafts [@bbs-draft]; and Damgard&apos;s notes together with Trail of Bits&apos; ZKDocs when you need the correct Fiat-Shamir recipe written out [@damgard], [@tob-part1].&lt;/p&gt;
&lt;p&gt;Most of the ways this goes wrong are the same handful of mistakes phrased for different audiences -- and they are worth naming head-on, because reviewers find them in real code every week.&lt;/p&gt;
&lt;h2&gt;13. Common Misuse, and the Questions Engineers Ask&lt;/h2&gt;
&lt;p&gt;The antipattern catalog is short because the failures are few and repeat. Reviewers find these in real code, and every one maps to a move.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Weak Fiat-Shamir in real code&lt;/td&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;statement omitted from the challenge hash&lt;/td&gt;
&lt;td&gt;bind the full statement and parameters (strong FS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reused or biased nonce&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;two transcripts share a commitment; extractor recovers the key&lt;/td&gt;
&lt;td&gt;fresh, uniform, per-session nonces; hedged derivation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assuming Pedersen is perfectly binding&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;it is only computationally binding&lt;/td&gt;
&lt;td&gt;design for computational binding; protect the trapdoor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equivocation via a known &lt;code&gt;log_g h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;someone knows the trapdoor&lt;/td&gt;
&lt;td&gt;derive &lt;code&gt;h&lt;/code&gt; by nothing-up-my-sleeve hash-to-curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling your own OR-proof or CDS composition&lt;/td&gt;
&lt;td&gt;all three&lt;/td&gt;
&lt;td&gt;simulator and challenge-splitting are subtle&lt;/td&gt;
&lt;td&gt;use a vetted implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive linear multisig under concurrency&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;linear responses solved by ROS&lt;/td&gt;
&lt;td&gt;MuSig2 (n-of-n) or FROST (t-of-n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treating credentials as &quot;just signatures&quot;&lt;/td&gt;
&lt;td&gt;Move 1-3&lt;/td&gt;
&lt;td&gt;they are Sigma proofs with selective disclosure&lt;/td&gt;
&lt;td&gt;model unlinkability and presentation proofs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;ROS means Random Oracle Substitution&quot;&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;mistranslated acronym&lt;/td&gt;
&lt;td&gt;it is an overdetermined, solvable linear system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflating BIP-340 with EdDSA&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;different encoding, nonce, and curve&lt;/td&gt;
&lt;td&gt;pin the exact standard you mean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;


No. A Pedersen commitment is perfectly *hiding* and only *computationally* binding. A party who knows the trapdoor $\log_g h$ can open a single commitment to any message they like, by solving one linear equation for a matching blinder [@pedersen], [@damgard]. This is why the second generator must be produced so that nobody knows that trapdoor, and why &quot;Pedersen is perfectly binding&quot; is a dangerous thing to write in a spec.


The full statement, all public parameters, the prior transcript, and a domain separator -- not just the commitment [@bpw]. Omitting the statement is weak Fiat-Shamir, the bug behind both the Helios voting break in 2012 and the Frozen Heart disclosures in 2022 [@bpw], [@tob-part1]. The habit that prevents it: write down what the proof is about, and confirm each item is inside the hash.


Yes, and seeing that is worth the price of admission. A Schnorr signature is Fiat-Shamir applied to the Schnorr identification protocol, with the verifier&apos;s challenge replaced by a hash of the public key, message, and commitment [@fiat-shamir], [@bip340]. A signature *is* a non-interactive proof of knowledge of the private key; the message just rides along in the challenge.


Don&apos;t. The CDS construction for AND/OR composition relies on splitting the challenge and simulating the branches you cannot answer, and getting the challenge algebra or the simulator subtly wrong breaks soundness or zero knowledge [@cds]. Use a vetted implementation; this is exactly the code that looks simple and is not.


No. It is forgeable under concurrent sessions -- sub-exponentially since Drijvers at S&amp;amp;P 2019, and in polynomial time since the ROS result in 2021 [@drijvers], [@ros]. Use MuSig2 for n-of-n signing or FROST for t-of-n threshold signing; both are engineered specifically to defeat that attack [@musig2], [@rfc9591].


No. They are Sigma-protocol proofs of knowledge of a signature or an algebraic MAC, and they support selective disclosure and unlinkability -- a plain signature has neither [@bbs-draft], [@signal]. Treating a credential presentation as an ordinary signature verification misses the entire privacy machinery that is the point of the primitive. Privacy Pass and Apple&apos;s Private Access Tokens are the single-*show* authorization-token cousin -- unlinkable, but a one-time &quot;you are allowed&quot; token rather than a multi-attribute selective-disclosure credential [@rfc9576].


No, and the mistranslation is everywhere online. ROS is &quot;Random inhomogeneities in an Overdetermined, Solvable system of linear equations&quot; -- a linear-algebra object, not an oracle trick [@ros]. The attack forges by solving a system of equations over responses collected across concurrent sessions, with no discrete logarithm ever solved.

&lt;h3&gt;The discrete log held; the three moves leaked&lt;/h3&gt;
&lt;p&gt;Follow the whole failure catalog back and it lands in one place. In April 2022, three unrelated proof systems were forgeable at once, and the discrete logarithm was untouched in all three [@tob-part1]. A decade earlier, the same weak-Fiat-Shamir bug had forged ballot proofs in Helios [@bpw]. A reused nonce hands over a private key by the same algebra that proves Schnorr sound [@bip340]. The ROS attack forges naive multisignatures in polynomial time by solving a linear system, no hard problem broken [@ros]. A Pedersen commitment equivocates the instant someone knows a trapdoor [@pedersen].&lt;/p&gt;
&lt;p&gt;Four decades, and the discrete logarithm never fell. Every named break -- Frozen Heart, Helios, nonce reuse, ROS, Drijvers, equivocation -- landed on one of the three moves built on top of it.&lt;/p&gt;
&lt;p&gt;And the composites climbed while the foundations stayed put. Borromean gave way to Bulletproofs and Bulletproofs+ [@bulletproofs], [@bulletproofs-plus]; naive multisignatures gave way to MuSig2 and FROST [@musig2], [@rfc9591]; Chaum&apos;s blind signatures gave way, in deployment, to Signal KVAC, BBS, and Privacy Pass [@signal], [@bbs-draft], [@rfc9576]. Not one of those ladders replaced Pedersen, Schnorr, or Fiat-Shamir. The three moves keep running underneath every rung, exactly as they were written between 1985 and 1996 -- and where the foundation moved at all, it was Move 2 being hardened in place, weak Fiat-Shamir corrected to strong, never a piece torn out and replaced.&lt;/p&gt;
&lt;p&gt;This is the frame the series keeps returning to from a new angle each time. Part 18 argued that &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/&quot; rel=&quot;noopener&quot;&gt;nobody broke the discrete log&lt;/a&gt;; Part 17 that the math held and the interface leaked. Here, the math held and the &lt;em&gt;conversation&lt;/em&gt; leaked -- because a discrete-log zero-knowledge proof was never one thing. It was always three moves, and each one is a separate promise that can be broken without touching the problem underneath.&lt;/p&gt;

The discrete log held. The three moves leaked.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;commitments-and-sigma-protocols-pedersen-schnorr-and-the-dis&quot; keyTerms={[
  { term: &quot;Sigma protocol&quot;, definition: &quot;A three-move public-coin proof -- commit, challenge, respond -- with special soundness and special honest-verifier zero knowledge, checked by one verification equation.&quot; },
  { term: &quot;Special soundness&quot;, definition: &quot;Two accepting transcripts sharing a commitment but with different challenges yield the witness via an extractor; what makes a Sigma protocol a proof of knowledge.&quot; },
  { term: &quot;SHVZK&quot;, definition: &quot;Special honest-verifier zero knowledge: any accepting transcript can be simulated without the witness by choosing the response and challenge first, proving the exchange reveals nothing.&quot; },
  { term: &quot;Pedersen commitment&quot;, definition: &quot;C = g^m times h^r: perfectly hiding, computationally binding on the discrete log, additively homomorphic -- and NOT perfectly binding, since a trapdoor holder can equivocate.&quot; },
  { term: &quot;Fiat-Shamir transform&quot;, definition: &quot;Replacing the interactive verifier&apos;s challenge with a hash of the transcript, making a Sigma protocol non-interactive. Strong FS binds the whole statement; weak FS omits it and is forgeable.&quot; },
  { term: &quot;Forking lemma&quot;, definition: &quot;The rewinding proof that turns a random-oracle forger into a discrete-log solver, giving Schnorr its ROM security -- loosely, with a quadratic loss, and only for single sessions.&quot; },
  { term: &quot;ROS problem&quot;, definition: &quot;Random inhomogeneities in an Overdetermined, Solvable system of linear equations; solvable in polynomial time above about log2(p) concurrent sessions, breaking naive linear Schnorr multi and blind signatures.&quot; },
  { term: &quot;MuSig2 vs FROST&quot;, definition: &quot;MuSig2 (BIP-327) is n-of-n two-round Schnorr multisig with a nonlinear two-nonce trick; FROST (RFC 9591) is t-of-n threshold Schnorr. Both defeat ROS.&quot; },
  { term: &quot;KVAC vs BBS&quot;, definition: &quot;Signal KVAC is a keyed-verification credential (issuer verifies, algebraic MAC, deployed); BBS is a publicly-verifiable multi-message credential on BLS12-381 (standardizing).&quot; },
  { term: &quot;Privacy Pass / PAT&quot;, definition: &quot;An unlinkable single-use authorization token (RFC 9576/9577/9578, June 2024), deployed as Apple Private Access Tokens; the single-show cousin of credentials, not multi-attribute selective disclosure.&quot; }
]} questions={[
  { q: &quot;A proof verifies but the statement is false, and nobody solved the discrete log. Which move failed, and how?&quot;, a: &quot;Move 2, the challenge. Weak Fiat-Shamir omitted the statement from the hash, so a valid proof was transplanted onto a false statement -- the Frozen Heart and Helios bug.&quot; },
  { q: &quot;Two Schnorr signatures share a nonce. What can an attacker compute, and why is it the same math as a soundness proof?&quot;, a: &quot;The private key, via x = (z1 - z2)/(e1 - e2). That is exactly the special-soundness extractor from two transcripts on the same commitment, run by the attacker instead of the security proof.&quot; },
  { q: &quot;Why is a naive two-round Schnorr multisignature insecure under concurrency when single-session Schnorr is proven secure?&quot;, a: &quot;The forking-lemma proof does not compose across concurrent sessions, and the linear response lets the ROS attack solve a system over many open sessions to forge -- with no discrete log solved. MuSig2&apos;s nonlinear two-nonce trick evades it.&quot; },
  { q: &quot;Is a Pedersen commitment perfectly binding, and what is the security-critical setup step?&quot;, a: &quot;No -- it is perfectly hiding and only computationally binding. The critical step is deriving the second generator h with an unknown discrete log, by hashing to the curve, so no party holds the equivocation trapdoor.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>commitments</category><category>sigma-protocols</category><category>pedersen-commitment</category><category>schnorr</category><category>fiat-shamir</category><category>zero-knowledge-proofs</category><category>bulletproofs</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>