Transcript & Randomness
The random/ module implements Longfellow’s Fiat-Shamir transcript and the prover-side secure RNG. A single Transcript object flows through the entire proof — sumcheck appends round polynomials, Ligero appends commitment roots and derives query indices, zk/ seeds everything with the circuit ID and public inputs. If you instantiate the prover’s and verifier’s transcripts symmetrically — same seed, same writes in the same order — they derive identical challenges, and the protocol is sound.
When to reach for it
- You’re wiring prover and verifier and need to make sure their transcripts match.
- You want to add your own domain-separation tag to the protocol (for example, per-tenant or per-version).
- You’re debugging a nondeterministic verification failure — almost always a transcript mismatch.
Design overview
Transcript state. Transcript wraps a SHA-256 hash state. Writes append tagged, length-prefixed data (bytestrings, field elements, arrays of field elements). Each bytes(...) call snapshots the current SHA-256 state, derives an AES-256 key, and streams pseudorandom output via AES-ECB — an FSPRF (Fiat-Shamir PRF) that stays stable until the next write(...) invalidates the snapshot. This ensures that the same bytestring always produces the same challenge bytes until the transcript is mutated.
Wrappers. TranscriptSumcheck<Field> (in sumcheck/) and LigeroTranscript<Field> (in ligero/) impose per-protocol write discipline — sumcheck appends round polynomials and derives scalar challenges; Ligero appends commitment roots, derives linear-combination coefficients, and derives nreq query indices. Both wrappers operate on the same underlying Transcript, so challenges in one sub-protocol depend cumulatively on the other’s appends. This is what makes the non-interactive reduction sound: you cannot tamper with Ligero’s challenges without changing the sumcheck challenges too, and vice versa.
Domain separation. Not via explicit tags, but via the tag byte each typed write prepends (bytestring, field element, array) plus the length prefix. Two different typed writes would need to produce byte-identical transcript bytes to collide, which the tags prevent by construction. Callers can add extra domain separation by seeding the transcript with a protocol-unique bytestring in the constructor: Transcript((uint8_t*)"my-protocol-v1", 14).
Prover vs. verifier RNG. The transcript itself is deterministic on both sides. The prover additionally needs genuine randomness for Ligero’s zero-knowledge blinding and for Merkle leaf salting; SecureRandomEngine (in random/secure_random_engine.h) wraps OpenSSL’s RAND_bytes() for that. The verifier never instantiates a random engine; everything it derives comes from the transcript.
API surface
Transcript
- Constructor:
Transcript(const uint8_t init[], size_t init_len)— seed with init bytes. - Writes:
void write(const uint8_t data[], size_t n),void write(const typename Field::Elt&, const Field&),void write(const Elt[], size_t ince, size_t n, const Field&),void write0(size_t n)(append n zero bytes). - Readers:
void bytes(uint8_t* buf, size_t n)returns pseudorandom output derived from the current transcript state.
Transcript challenge derivation
elt, nat, and choose are inherited from the abstract base class RandomEngine (random/random.h), which defines them in terms of a single virtual bytes(uint8_t*, size_t). Transcript overrides bytes() to stream deterministic FSPRF output keyed from the current SHA-256 transcript state; SecureRandomEngine overrides the same bytes() to stream OS entropy via OpenSSL RAND_bytes. The methods themselves are identical — the determinism comes from which subclass’s bytes() they ultimately call. Calling them on a Transcript therefore yields Fiat-Shamir challenges reproducible on both prover and verifier; calling them on a SecureRandomEngine yields non-reproducible randomness used only by the prover for zero-knowledge blinding.
typename Field::Elt elt(const Field&)— derive a pseudorandom field element challenge.size_t nat(size_t n)— derive a uniform pseudorandom integer challenge in[0, n).void choose(size_t res[], size_t n, size_t k)— derivekdistinct pseudorandom indices from[0, n).
Secure RNG
SecureRandomEngine()— zero-arg constructor, wraps OpenSSLRAND_bytes.void bytes(uint8_t* buf, size_t n)— fillsbufwith OS entropy.
Example
// Different field elements written → different challenges derived.
uint8_t buf1[4], buf2[4];
Transcript ts1((uint8_t*)"test", 4);
ts1.write(F.of_scalar(7), F);
ts1.bytes(buf1, 4);
Transcript ts2((uint8_t*)"test", 4);
ts2.write(F.of_scalar(8), F);
ts2.bytes(buf2, 4);
// buf1 != buf2See it used
- transcript_test.cc — unit tests
- transcript_sumcheck.h — sumcheck wrapper
- ligero_transcript.h — Ligero wrapper
Related
- ZK (top-level) — instantiates the top-level transcript
- Sumcheck
- Ligero