Fiat–Shamir transform

from interactive proofs and verifier randomness to hash-derived challenges, transcripts, random oracles, and non-interactive ZK proofs

Why do we need the Fiat–Shamir transform?

Start with an interactive proof.

A prover wants to convince a verifier that some statement is true. The verifier sends unpredictable challenges, and the prover responds.

Prover → commitment → Verifier
Prover ← random challenge ← Verifier
Prover → response → Verifier

This interaction is useful because the verifier's challenge is unpredictable to the prover when the commitment is created.

But an interactive protocol has a practical problem: the prover and verifier must communicate during the proof.

The Fiat–Shamir transform removes that interaction by replacing the verifier's random challenge with a cryptographic hash of the transcript.

Mental model: instead of asking a verifier for a random challenge, the prover computes the challenge as a hash of everything that came before it. The transcript becomes self-contained.

First understand an interactive proof

Most Fiat–Shamir explanations become much easier if you first understand the interactive protocol it transforms.

A common three-message public-coin protocol has this shape:

Prover → Verifier : commitment A
Verifier → Prover : challenge c
Prover → Verifier : response z

The verifier checks:

Verify(statement, A, c, z) = accept

The important property is that c is chosen after A.

The prover commits first, before knowing which challenge it will have to answer.

What does “public-coin” mean?

In a public-coin protocol, the verifier's message is essentially a random challenge.

There is no secret verifier state that the prover needs to learn.

commitment A
random challenge c ← ChallengeSpace
response z

This matters because Fiat–Shamir is designed around replacing this public random challenge with a deterministic value derived from the transcript.

The central Fiat–Shamir idea

Instead of:

c ←$ ChallengeSpace

the prover computes:

c = H(statement || A)

or, more generally:

c = H(transcript_so_far)

The response is then computed using that challenge:

z = Respond(witness, A, c)

The verifier independently computes the same hash and therefore derives the same challenge.

statement + commitment A → H → challenge c
A, c, z verification

The transcript is the key

The verifier must derive the exact same challenge as the prover.

Therefore both parties need the same transcript bytes.

statement = public statement bytes
A = commitment bytes
c = H(statement || A)
z = response

This is why real proving systems use transcript objects rather than casually concatenating arbitrary values.

The transcript typically defines:

  • what gets absorbed into the hash;
  • the exact byte encoding;
  • domain-separation labels;
  • when a challenge is sampled;
  • how the digest becomes a field element or other challenge type.
Implementation rule: Fiat–Shamir is not simply “call SHA-256 somewhere.” The transcript format is part of the cryptographic protocol.

Concrete example: Schnorr identification

Consider a cyclic group with generator g.

The prover knows a secret:

x

and the public key is:

y = gˣ

The prover wants to demonstrate knowledge of x without revealing it.

Step 1 — commitment

The prover chooses random:

r ←$ Z_q

and computes:

A = gʳ

Step 2 — verifier challenge

In the interactive protocol:

c ←$ Z_q

Step 3 — response

The prover sends:

z = r + cx mod q

Step 4 — verification

The verifier checks:

gᶻ = A · yᶜ

Why?

gᶻ
= gʳ⁺ᶜˣ
= gʳ · (gˣ)ᶜ
= A · yᶜ

Apply Fiat–Shamir to Schnorr

Replace the verifier's random challenge with a hash:

c = H(y || A || message)

The prover computes:

z = r + cx mod q

The final proof is approximately:

π = (A, z)

The verifier receives the public key, message, and proof. It recomputes:

c = H(y || A || message)

and checks:

gᶻ = A · yᶜ

No interactive challenge was required.

This is the transformation: the verifier's random challenge becomes a deterministic function of the transcript.

Why can't the prover choose the challenge?

This is the heart of the soundness intuition.

Suppose the prover could choose both:

A
c

before committing to the protocol state.

Then a malicious prover might construct a response specifically for a challenge it already knows.

Interactive protocols prevent this by forcing:

A first → c later

Fiat–Shamir tries to preserve that ordering cryptographically:

A first → H(A) → c

The prover cannot choose c independently after fixing A; the challenge is tied to the commitment through the hash.

The random-oracle viewpoint

Security proofs for Fiat–Shamir are often described using the random oracle model.

In this idealized model, a hash function behaves like a random function:

H(input) → unpredictable random-looking output

For an input that has not previously been queried, the output is modeled as unpredictable.

This resembles the verifier's random challenge:

interactive: c ← random
Fiat–Shamir: c = H(transcript)

In the random-oracle model, the second value can behave like a fresh random challenge once the transcript has been fixed.

Important distinction: the random-oracle model is an idealized security model. A real hash function is not literally a mathematical random oracle.

Where does soundness come from?

Suppose a false statement can only be answered successfully for a small fraction of possible challenges.

For example, imagine a false prover succeeds for at most:

1 / |ChallengeSpace|

of all possible challenges.

In the interactive protocol, the verifier independently chooses the challenge, so the prover cannot reliably predict it.

With Fiat–Shamir, the challenge is generated from the commitment:

c = H(statement || A)

Under the random-oracle heuristic, the prover cannot predict or freely program this challenge in the ordinary execution of the protocol.

This preserves the basic intuition that a false prover must get lucky with the challenge.

The special role of rewinding in proofs

Interactive-proof security proofs often use a technique called rewinding.

Conceptually, a simulator or extractor may run a prover, observe a commitment, and then obtain responses to different challenges.

same A + challenge c₁ → z₁
same A + challenge c₂ → z₂

Two valid responses to different challenges can reveal useful algebraic information.

For Schnorr, for example:

z₁ = r + c₁x
z₂ = r + c₂x

Subtract:

z₁ − z₂ = (c₁ − c₂)x

so:

x = (z₁ − z₂)/(c₁ − c₂)

This illustrates why a malicious prover that can answer many different challenges consistently may be forced to know the underlying witness.

What about zero knowledge?

Fiat–Shamir itself is not a zero-knowledge property.

It is a transformation that removes interaction from certain public-coin protocols.

Whether the resulting proof is zero knowledge depends on the underlying protocol and its security proof.

fiat–shamir

Transforms an interactive public-coin protocol into a non-interactive one.

soundness

Concerns whether false statements can be proven successfully.

zero knowledge

Concerns whether the proof reveals information beyond validity of the statement.

completeness

Concerns whether an honest prover can convince the verifier of a true statement.

Transcript design is cryptographic design

A common beginner mistake is:

c = hash(some_values)

without specifying exactly which values and how they are encoded.

A robust transcript should make the protocol state explicit.

domain_separator
|| protocol_version
|| public_statement
|| commitment
|| previous_challenges

The exact fields depend on the protocol.

Why include the statement?

The challenge should be bound to the statement being proved. Otherwise the same commitment could potentially be reused across different statements in ways the protocol did not intend.

Why include a domain separator?

Different protocols should not accidentally interpret the same hash input as the same challenge.

Why specify encoding?

If two implementations serialize the same mathematical object differently, they will derive different challenges and the proof will fail verification.

Turning a hash into a field challenge

ZK systems frequently need a challenge in a finite field:

c ∈ F

But a cryptographic hash gives bytes.

So the transcript must define a conversion:

digest = H(transcript)
c = DecodeToField(digest)

For a simple educational field, one might reduce an integer modulo the field modulus:

c = integer(digest) mod p

Real protocols may use rejection sampling or specialized challenge-generation rules to avoid undesirable statistical bias or other issues.

Protocol detail: “hash and take mod p” is an educational simplification. Production challenge derivation must follow the exact scheme specification.

Fiat–Shamir with multiple rounds

ZK protocols often have many challenge points.

For example:

A₁ → c₁ → A₂ → c₂ → A₃ → c₃ → …

The transcript is extended after every message.

c₁ = H(transcript up to A₁)
c₂ = H(transcript up to A₂)
c₃ = H(transcript up to A₃)

This creates a deterministic chain of challenges.

A₁ → H → c₁ A₂ → H → c₂

In a transcript implementation, this often looks like:

absorb(A₁)
c₁ = squeeze_challenge()
absorb(A₂)
c₂ = squeeze_challenge()

Fiat–Shamir inside STARKs

Fiat–Shamir is fundamental to non-interactive STARK-style protocols.

A simplified STARK flow is:

execution trace polynomials Merkle commitment
commitment → H → random field challenge composition / folding
new commitment → H → next challenge

The prover does not wait for a network message from a verifier after every commitment. Instead, the transcript determines the challenges.

This is what turns an interactive public-coin proof into a non-interactive proof that can be generated and verified independently.

Fiat–Shamir inside FRI

FRI repeatedly commits to evaluation vectors and derives challenges that determine how the prover folds them.

Conceptually:

commit L₀
α₀ = H(transcript)
fold L₀ → L₁
commit L₁
α₁ = H(transcript)
fold L₁ → L₂

The verifier recomputes the same challenges from the transcript.

This is why transcript correctness is not a cosmetic implementation detail in FRI or STARK code. A different byte encoding or challenge order means a completely different proof.

Fiat–Shamir transcript in Rust

Here is a small educational transcript implementation. It demonstrates the core pattern: absorb protocol data, hash the transcript, and derive a challenge.

Complete Rust implementation
rust
// Cargo.toml:
// sha2 = "0.10"

use sha2::{Digest, Sha256};

struct Transcript {
    state: Vec<u8>,
}

impl Transcript {
    fn new(domain: &[u8]) -> Self {
        let mut state = Vec::new();

        // Domain separation.
        state.extend_from_slice(domain);

        Self { state }
    }

    fn append_message(
        &mut self,
        label: &[u8],
        message: &[u8],
    ) {
        // Length prefixes make the encoding explicit.
        self.state
            .extend_from_slice(&(label.len() as u64).to_le_bytes());

        self.state.extend_from_slice(label);

        self.state
            .extend_from_slice(&(message.len() as u64).to_le_bytes());

        self.state.extend_from_slice(message);
    }

    fn challenge_bytes(&self) -> [u8; 32] {
        let digest = Sha256::digest(&self.state);
        digest.into()
    }

    fn challenge_u64(&self) -> u64 {
        let digest = self.challenge_bytes();

        let mut bytes = [0u8; 8];
        bytes.copy_from_slice(&digest[..8]);

        u64::from_le_bytes(bytes)
    }
}

fn main() {
    let commitment = b"commitment-A";

    let transcript =
        Transcript::new(b"example-protocol-v1")
            .append_message(
                b"commitment",
                commitment,
            );

    let challenge = transcript.challenge_u64();

    println!("challenge = {challenge}");
}

This is deliberately simple. A production transcript should be designed around the exact protocol and should use an appropriate challenge-generation construction rather than treating arbitrary bytes as a complete cryptographic specification.

A more realistic STARK-style transcript pattern

In a proving system, the transcript often follows a pattern like:

transcript.append(commitment_0)
challenge_0 = transcript.challenge_field()
transcript.append(commitment_1)
challenge_1 = transcript.challenge_field()
transcript.append(commitment_2)
challenge_2 = transcript.challenge_field()

The critical invariant is:

prover transcript = verifier transcript

If they diverge by even one byte, every subsequent challenge can diverge.

Why transcript binding matters

Suppose a challenge were generated only from a commitment:

c = H(A)

But the proof is actually intended to establish a statement S.

It is generally safer for the challenge derivation to bind the relevant statement and protocol context:

c = H(domain || S || A)

Otherwise a transcript might be reusable in a context where the protocol did not intend it to be reusable.

Exact transcript binding requirements are protocol-specific, but the general principle is universal: hash the data that the challenge is logically supposed to depend on.

The forking intuition

Security proofs for Fiat–Shamir often use a forking argument.

Very roughly, imagine a successful malicious prover that produces:

(A, c, z)

If a reduction can obtain another accepting transcript with the same commitment but a different challenge:

(A, c′, z′)

then the two accepting transcripts may reveal information that contradicts the prover's supposed ability to prove a false statement without the witness.

For Schnorr, this is exactly the algebra behind extracting the secret:

x = (z − z′)/(c − c′)

The full security theorem is more subtle than this intuition, but the example shows why challenge unpredictability and transcript binding are central.

Fiat–Shamir is not magic

not every protocol

Fiat–Shamir is not a universal compiler for arbitrary interactive protocols. The public-coin structure and security properties of the underlying protocol matter.

model matters

A proof in the random-oracle model does not automatically become a proof in every standard model.

transcript matters

Leaving values out of the transcript, using ambiguous encodings, or deriving challenges at the wrong point can invalidate the intended security argument.

challenge conversion matters

Mapping hash output into a finite field or challenge space needs to follow the protocol's specified method.

Interactive vs Fiat–Shamir

PropertyInteractive protocolFiat–Shamir version
challenge sourceverifier randomnesshash of transcript
communicationmultiple roundssingle proof object
public coinyessimulated through hashing
verificationinteractiveoffline
transcriptmessages exchanged liveproof contains enough data to reconstruct challenges
security intuitionunpredictable verifier challengeunpredictable hash-derived challenge

How it fits into a ZK proving pipeline

witness commitment Fiat–Shamir challenge
challenge algebraic response new commitment
new transcript → H → next challenge

For a STARK, this can involve commitments to trace-derived evaluation vectors, random field challenges, composition-polynomial combinations, FRI commitments, and query positions.

For a SNARK, Fiat–Shamir can be used to derive challenges in polynomial interactive-oracle protocols, inner-product arguments, and other proof components.

The recurring pattern is:

commit → hash transcript → challenge → respond → extend transcript → repeat

The whole idea in one map

first-principles map
1. An interactive public-coin proof has a prover commitment A.
2. The verifier chooses a fresh random challenge c.
3. The prover answers with a response z.
4. Fiat–Shamir replaces c ← random with c = H(transcript).
5. The prover and verifier therefore derive the same challenge independently.
6. The proof becomes non-interactive.
7. The transcript must bind every value the challenge is supposed to depend on.
8. Hash output must be converted into the required challenge space correctly.
9. Security is commonly analyzed in the random-oracle model.
10. The transformation does not itself mean “zero knowledge.”
11. STARKs and FRI use Fiat–Shamir to replace verifier challenges throughout the proving protocol.

If Merkle trees answer “how do we commit to a large vector?”, and polynomial commitments answer “how do we commit to a polynomial?”, Fiat–Shamir answers “how can a public-coin interactive proof generate its own verifier challenges from a cryptographic transcript?”