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

Archetype x zkSecurity (Whiteboard Session) - Proof is in the Pudding: zkTLS

In our "Proof is in the Pudding" series, hosted with Archetype, we dive into the world of zkTLS, also known as zkOracles, HTTPz, or MPC-TLS. You'll get the inside scoop on various approaches like public oracles, TEE methods such as TownCrier, and hybrid models using MPC protocols. It's a perfect chance to explore cutting-edge TLS technologies and see how they shape secure communication. Check out the recorded session on Archetype's channel!

ZK/SEC · October 16, 2024

zkVM Security: What Could Go Wrong?

Ever wondered how zkVMs simplify the use of zero-knowledge proofs in coding? We dive into how they let developers focus more on application logic by abstracting complex cryptographic aspects, using familiar languages like Rust or C++. But hold on, it's not all smooth sailing. Despite these benefits, a single bug anywhere in the complex system of compilers, proof systems, or verification can lead to serious security issues. In the post, we break down the zkVM workflow, explore common vulnerabilities at each phase, and highlight the importance of understanding these layers to build more secure, zk-powered applications. Curious about how this all plays out? Let’s unravel it together!

Suneal Gong · November 21, 2024

Introducing clean, a formal verification DSL for ZK circuits in Lean4

We're diving into our new project called **clean**, aimed at creating an embedded DSL and formal verification framework for Zero Knowledge (ZK) circuits using Lean4. Imagine being able to not only define ZK circuits but also formally prove their correctness. Sounds like a game-changer, right? We'll walk you through our process of building a robust library of reusable, verified circuit gadgets, focusing on the importance of soundness and completeness. Plus, you'll get a peek at some cool examples like 8-bit addition and how we're tackling ZKVM design with techniques borrowed from Fibonacci sequences. It's exciting stuff, and if you're curious about how we're paving the way for bug-free ZK circuits, this is a read you won't want to miss!

Giorgio Dell'Immagine · March 27, 2025