ZK/SEC Research notes from zkSecurity
All posts
educative · zk · halo2

Halo2's Elegant Transcript As Proof

halo2

Today I want to showcase something really cute that zcash's halo2 implementation has designed in order to implement Fiat-Shamir in a secure way.

If you take a look at their plonk prover, you will see that a mutable transcript is passed and in the logic, you can see that the transcript absorbs things differently:

  • transcript.common_point() is used to absorb instance points (points that the prover and the verifier both know)
  • transcript.write_point() absorbs messages that in the interactive version of the protocol would be sent to the verifier
  • transcript.write_scalar() same but for scalars
  • transcript.squeeze_challenge_scalar() is used to generate verifier challenges

What is interesting are the prover-only functions write_point and write_scalar implementations. If we look at how the transcript is implemented, we can see that it does two things:

  1. It hashes the values in a Blake2b state. This is the usual Fiat-Shamir stuff we're used to seeing. This is done in the common_point and common_scalar calls below.

  2. It also writes the actual values in a writer buffer. This is what I want to highlight in this post, so keep that in mind.

    fn write_point(&mut self, point: C) -> io::Result<()> {
        self.common_point(point)?;
        let compressed = point.to_bytes();
        self.writer.write_all(compressed.as_ref())
    }
    fn write_scalar(&mut self, scalar: C::Scalar) -> io::Result<()> {
        self.common_scalar(scalar)?;
        let data = scalar.to_repr();
        self.writer.write_all(data.as_ref())
    }

On the other side, the verifier starts with a fresh transcript as well as the buffer created by the prover (which will act as a proof, as you will see) and uses some of the same transcript methods that the prover uses, except when it has a symmetrical equivalent. That is, instead of acting like it's sending points or scalars, it is using functions to receive them from the prover. Mind you, this is a non-interactive protocol so the implementation really emulates the receiving of prover values. Specifically, the verifier uses two types of transcript methods here:

  • read_n_points(transcript, n) reads n points from the transcript
  • read_n_scalars(transcript, n) does the same but for scalars

What is really cool with this abstraction, is that the absorption of the prover values with Fiat-Shamir happens automagically and is enforced by the system. The verifier literally cannot access these values without reading (and thus absorbing) them.

It is important to repeat: all values sent by the prover are magically absorbed in Fiat-Shamir, leaving no room for most Fiat-Shamir bug opportunities to arise.

We can see the magic happening in the transcript code:

    fn read_point(&mut self) -> io::Result<C> {
        let mut compressed = C::Repr::default();
        self.reader.read_exact(compressed.as_mut())?;
        let point: C = Option::from(C::from_bytes(&compressed)).ok_or_else(|| {
            io::Error::new(io::ErrorKind::Other, "invalid point encoding in proof")
        })?;
        self.common_point(point)?;

        Ok(point)
    }

    fn read_scalar(&mut self) -> io::Result<C::Scalar> {
        let mut data = <C::Scalar as PrimeField>::Repr::default();
        self.reader.read_exact(data.as_mut())?;
        let scalar: C::Scalar = Option::from(C::Scalar::from_repr(data)).ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::Other,
                "invalid field element encoding in proof",
            )
        })?;
        self.common_scalar(scalar)?;

        Ok(scalar)
    }

Here the buffer is called reader, and is the buffer at the end of the proof creation. The common_point calls are the ones that mirror the absorption in the transcript that the prover did on their side.

Keep reading
Latest

The Year Finding and Exploiting Bugs Became Cheap, and What to Do About It

The economics of security have changed. AI has made finding and exploiting bugs cheaper, including in cryptographic and zero-knowledge code, while validation and remediation remain slow. Here is our view of what happened, what comes next, and how teams should change the way they secure their stack to defend against AI-assisted attackers.

Stefanos Chaliasos · September 07, 2026

Variants of KZG: Part V, Multilinear Commitments with Mercury

In this final post of the series, we extend univariate KZG commitments to multilinear polynomials through Mercury. Building on Gemini, we fold half of the variables at once, use polynomial division to bind this large fold to the original commitment and reduce the remaining multilinear evaluations to a batched inner product check. We then walk through the end-to-end opening protocol. We conclude by examining its proof size, prover cost and verifier cost.

Varun Thakore · August 17, 2026

Variants of KZG: Part IV, Multilinear Commitments with Gemini

In this blog post, we extend univariate KZG commitments to multilinear polynomials through Gemini. We introduce recursive partial evaluations, derive the split-and-fold identity and express each fold as a univariate identity that can be checked using KZG openings. We then walk through the end-to-end opening protocol. We conclude by examining its proof size, prover cost, and verifier cost.

Varun Thakore · August 12, 2026
Recommended

Become a Halo2 Hero: Master Zero-Knowledge Proofs with Our New Course

We're teaming up with the Zircuit team to bring you a new course on Halo2 development that's perfect for Rust developers eager to dive into creating Halo2 circuits from scratch. No need to be a cryptographer or have prior knowledge of Halo2, PlonK, or zkSNARKs. Our course starts with the basics and guides you through building increasingly complex circuits. By the end, you'll be a Halo Hero! Plus, you'll have access to complete, runnable code examples on GitHub. Ready to start your Halo2 journey? Check it out!

Mathias Hall-Andersen · November 14, 2024

Uncovering the Query Collision Bug in Halo2: How a Single Extra Query Breaks Soundness

We recently discovered a subtle but important soundness issue in Halo2, which we’ve named the query collision bug. It affects certain edge-case circuits and was present in widely used versions, including the main Zcash implementation and PSE’s fork. We disclosed the issue to the relevant teams, including Zcash, PSE, and Axiom, all of whom have since patched it. While no known production circuits were affected, the bug reveals a surprising vulnerability in the proving system that deserves attention.

Suneal Gong · July 09, 2025

Breaking Down Bulletproofs: No Pairings, No Trusted Setup

Learn how Bulletproofs enables efficient zero-knowledge proofs without trusted setups by computing inner products in a verifiable way. This post breaks down the core folding technique that reduces large vectors to single elements through recursive compression, making proofs both compact and fast to verify. Used in Monero, Mina's Kimchi, and Zcash's Halo 2, Bulletproofs is a practical alternative to pairing-based schemes.

David Wong · October 23, 2025
More to explore

Common Circom Pitfalls and How to Dodge Them, Part 1

Programming in Circom comes with its fair share of challenges. After reviewing numerous Circom codebases, we’ve identified certain anti-patterns that occur frequently. In this series, we’ll provide a comprehensive overview of these issues to help you avoid the most common pitfalls. Of course, this won’t be a complete list of every mistake possible (Circom has plenty of ways to trip you up). But the footguns we’ll cover are the ones that tend to catch developers off guard the most.

Marco Besier · June 24, 2025

Auditing Self: Collaborating with Celo on Privacy and Identity Infrastructure

We recently teamed up with Celo for an in-depth security audit of the Self project, exploring its innovative approach to on-chain identity using biometric passports and zero-knowledge proofs. During our three-week dive, we examined everything from cryptographic primitives to smart contract architecture and a unique proof delegation system using AWS Nitro Enclaves. The Celo team impressed us with their commitment and responsiveness, and we collaborated on refining the system with strategic improvements. Curious about the nitty-gritty details and our insights? Check out the full report!

ZK/SEC · March 24, 2025

AI meets Cryptography 3: What AI Found in Bron Labs's bron-crypto

We pointed our AI audit pipeline at bron-crypto, Bron Labs's Go library for MPC and threshold signatures, and confirmed four bugs. All of them are now fixed upstream. This is the third post in our series on bugs our agents found across open source cryptography.

Stefanos Chaliasos, Hao Pham, False Witness Team · July 22, 2026