2026-10-10

This month

Fiat-Shamir Transcript Bugs: Why Proof Systems Must Hash Everything the Prover Sends

Produced by the ZK Security research pipeline. Published only after the automated quality gate. The gate is described in the editorial policy.

Most zero-knowledge proofs deployed in blockchains are non-interactive. The prover publishes a single proof, and anyone can check it without a back-and-forth conversation with a verifier. The Fiat-Sha…

Fiat-Shamir Transcript Bugs: Why Proof Systems Must Hash Everything the Prover Sends

Most zero-knowledge proofs deployed in blockchains are non-interactive. The prover publishes a single proof, and anyone can check it without a back-and-forth conversation with a verifier. The Fiat-Shamir transformation makes this possible. Instead of a verifier sending random challenges, the challenges are derived by hashing a transcript of the protocol so far. The security of the whole construction depends on that transcript being complete. If any value the prover controls is left out of the hash, the prover can choose it after seeing the challenge, and soundness can collapse.

This class of bug has hit academic papers, widely used proof libraries and production blockchain programs. This guide explains how it works, reviews two well-documented cases, and sets out the checks that prevent it.

How a weak Fiat-Shamir implementation fails

In an interactive sigma protocol, the prover commits to a value, the verifier sends a random challenge, and the prover responds. Soundness relies on ordering: the prover is bound to the commitment before learning the challenge. Fiat-Shamir replaces the verifier's randomness with a hash, so the challenge must be computed over everything the prover is supposed to be bound to, including the public statement and every commitment.

If a public input or a prover message is missing from the hash, the challenge no longer depends on it. A malicious prover can then fix the challenge first and solve backwards for the missing value so that the verification equation holds. The result is a proof that verifies for a statement that is false.

Frozen Heart: weak Fiat-Shamir across major libraries

Trail of Bits named this vulnerability class "Frozen Heart" in a coordinated disclosure on April 13, 2022. In the first post of the series, Jim Miller described it as "insecure implementations of the Fiat-Shamir transformation that allow malicious users to forge proofs for random statements." Affected implementations included ZenGo's zk-paillier, ING Bank's zkrp, SECBIT Labs' ckb-zkp, Adjoint's bulletproofs, Dusk Network's PlonK, Iden3's SnarkJS and ConsenSys' gnark.

The post also found that the Bulletproofs flaw originated in the original academic paper, which recommended an insecure way to generate the Fiat-Shamir challenge. Implementations that followed the paper faithfully inherited the bug. By the time of disclosure, ZenGo, Dusk Network, Iden3 and ConsenSys had patched their code, ING Bank had deleted its repository, and the Bulletproofs authors had updated their paper.

Trail of Bits' recommendation was direct: "the Fiat-Shamir hash computation must include all public values from the zero-knowledge proof statement," along with all commitment values the prover generates.

Phantom Challenge: an omitted challenge in Solana's ZK ElGamal program

The same principle applies to composed protocols, where one proof contains several sub-proofs. In a June 2025 write-up, zkSecurity researcher Suneal Gong described a bug in PercentageWithCapProof, a sigma OR proof in Solana's ZK ElGamal Proof Program. The proof shows that an encrypted transfer fee is either a fixed percentage of the transfer amount or equal to a maximum fee.

In an OR proof, the prover simulates the branch that is false and chooses that branch's challenge itself. Here, the prover-generated challenge c_max_proof was not absorbed into the transcript before the verifier derived its randomizer. As the write-up puts it, "One algebraic component, a prover-generated 'challenge' value, was not absorbed into the transcript." A malicious prover could choose c_max_proof after seeing the randomizer and forge a proof that passed verification.

The impact reached past fee evasion. According to zkSecurity, an attacker could set an arbitrary encrypted fee and potentially mint or burn tokens under Token-2022's confidential transfer extension. The bug was reported to Anza through a GitHub Security Advisory with a proof of concept. Anza disabled the confidential transfer extension and then rolled out a validator patch disabling the ZK ElGamal Proof Program instruction. No in-the-wild exploitation was found. The fix was one line, appending c_max_proof to the transcript.

Defensive checklist

  • Hash everything the prover sends. zkSecurity's summary of the principle is the right default. Every commitment, every prover-chosen challenge in OR compositions, and every intermediate message belongs in the transcript before any challenge that depends on it is derived.
  • Bind the statement. Public inputs, parameters and domain separators belong in the transcript too. Omitting them is the classic Frozen Heart pattern.
  • Review composition boundaries. Bugs cluster where sub-proofs are combined, because each sub-proof may be correct on its own while the combined transcript is incomplete.
  • Do not trust the paper blindly. The Bulletproofs case shows that a reference construction can itself specify a weak transform.
  • Write forgery tests. For each sigma protocol, attempt to produce an accepting proof for a false statement by solving for an omitted value. A test that succeeds is a soundness bug.
  • Keep a kill switch. Solana's ability to disable the affected extension and instruction quickly limited exposure while the fix shipped.

Sources

Related guides: under-constrained circuits, auditing circom, choosing a proving system, trusted setup risk, the checklist.