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
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

Public report of Darkfi circuits and crypto audit

In February 2024, we dived into a security audit of Darkfi's built-in contracts and circuits, putting them under the microscope to ensure everything checks out. Curious to know what we discovered while navigating the public Darkfi repository? Check out our full report for insights and findings.

ZK/SEC · February 27, 2024

Faster Sumchecks: Part I

In this blog post, we explore how to optimize the sumcheck protocol, particularly when working with values in a small field and randomness from a large field, as often needed in zkVMs. We introduce various algorithms aimed at reducing expensive operations, focusing on minimizing large multiplications. Starting from using simple evaluation tables to more sophisticated techniques like precomputing accumulators and leveraging Lagrange interpolation, we demonstrate how to efficiently organize computations to speed up proving times. Readers will gain insights into handling arithmetic operations within the sumcheck protocol and learn about optimizing specific cases in zero-knowledge proofs.

Jason Park · November 21, 2025

FRIDA: Data-Availability Sampling from FRI

We've just dropped a blog post exploring the cool world of Ethereum's latest EIP-4844 upgrade and how it tackles the data-availability conundrum with Proto-Danksharding. Get ready to dive into the intricacies of making data more accessible (and less pricey) by using nifty concepts like blobs and polynomial evaluations. Plus, we dish on the role of FRI in data-availability sampling and how it can streamline things even more. Curious? Check out the details and see how this all shapes Ethereum's future!

Mathias Hall-Andersen · June 25, 2024