01 · Motivation

Start before the proof becomes bytes

A serialized proof, verification key, or public-input file looks like arbitrary binary because it is the final output of several layers: field elements, polynomial claims, transcript challenges, commitments, curve points, and a chosen encoding. Reading the bytes first hides the structure that makes the verifier believe them.

My study repository builds that structure upward. The modules cover finite-field polynomial arithmetic, interpolation, FFTs, multilinear extensions, sumcheck, GKR, and KZG experiments. Correctness and visibility take priority over performance; this is not presented as a production proving library.

The implementation uses arkworks field types over BN254 and a Keccak transcript. That makes the algebra concrete while keeping the code generic over PrimeField where possible.

02 · Sumcheck

The starting claim is a sum over the Boolean hypercube

Let f be an n-variable polynomial over a finite field. The prover claims that summing f over every Boolean assignment equals H. Enumerating all 2ⁿ points defeats the purpose for a verifier, so the protocol removes one variable per round.

In round one, the prover sends a univariate polynomial g₁(X) obtained by summing over every remaining Boolean variable. The verifier first checks g₁(0) + g₁(1) = H. It then samples a challenge r₁ and replaces the original claim with g₁(r₁). Round two repeats the process for the next variable.

H=b{0,1}nf(b)gi(0)+gi(1)=previous claim\begin{aligned} H &= \sum_{b\in\{0,1\}^{n}} f(b) \\ g_i(0)+g_i(1) &= \text{previous claim} \end{aligned}
After n rounds, the remaining claim is f(r₁,…,rₙ) at one transcript-derived point.
CLAIM

Prover states H, the sum over all Boolean evaluations.

ROUND POLY

Prover sends gᵢ for the next variable.

CONSISTENCY

Verifier checks gᵢ(0) + gᵢ(1) against the current claim.

CHALLENGE

Transcript derives rᵢ and folds the claim to gᵢ(rᵢ).

FINAL POINT

Verifier compares the final claim with f(r₁,…,rₙ).

03 · Representation

For a multilinear table, each round polynomial is determined by two values

The basic sumcheck module stores a multilinear polynomial as its evaluation table. split_and_sum divides that table into the xᵢ = 0 and xᵢ = 1 halves, then sums each half. Because the polynomial is multilinear in the active variable, those two values determine the entire round polynomial by linear interpolation.

The proof therefore stores each basic round as [gᵢ(0), gᵢ(1)]. After the transcript supplies rᵢ, partial evaluation folds the table and halves its length. This data movement is the protocol reduction made visible in code.

Build the two-point round polynomialrust
fn split_and_sum<F: PrimeField>(evals: &[F]) -> [F; 2] {
    let (left, right) = evals.split_at(evals.len() / 2);
    [left.iter().copied().sum(), right.iter().copied().sum()]
}

// g(r) from g(0), g(1)
let next_claim = g0 + challenge * (g1 - g0);

04 · Fiat–Shamir

The transcript replaces verifier messages - but ordering becomes security-critical

The interactive verifier would sample a fresh random challenge after seeing each round polynomial. The implementation makes the proof non-interactive with a Keccak transcript: absorb the original evaluation table and claimed sum, absorb each round polynomial, then squeeze a field element from the accumulated hash state.

Prover and verifier must absorb exactly the same bytes in exactly the same order. Any missing field, ambiguous serialization, or inconsistent domain separation can fork the transcript or allow messages from another protocol context to be replayed.

The current transcript is intentionally minimal. A hardened version should add protocol and version domain separators, length-prefix variable inputs, make squeeze mutate state explicitly, and include negative tests showing that reordered or omitted messages fail.

Verifier's round relationrust
for round_poly in proof.round_polys {
    if claimed_sum != round_poly.iter().sum() {
        return false;
    }

    transcript.absorb(&serialize(round_poly));
    let r: F = transcript.squeeze();
    claimed_sum = round_poly[0] + r * (round_poly[1] - round_poly[0]);
    challenges.push(r);
}

claimed_sum == original_poly.evaluate(&challenges)

05 · GKR

GKR applies the same reduction to a layered circuit

A layered arithmetic circuit gives each layer a table of wire values. Its multilinear extension Wᵢ lets the verifier ask for a layer value at a random point rather than inspect every wire. Wiring predicates addᵢ and mulᵢ encode which previous-layer wires feed each addition or multiplication gate.

For one output index a and child indices b and c, the layer relation combines those objects: an addition gate contributes Wᵢ₊₁(b) + Wᵢ₊₁(c); a multiplication gate contributes their product. Sumcheck reduces the large sum over b and c to evaluations of the previous layer at two random points.

The implementation then absorbs those W(b) and W(c) evaluations, derives alpha and beta, and folds the two claims into one linear combination for the next layer. Repeating this process walks the verifier from the output claim back toward the public input layer.

Fi(a,b,c)=addi(a,b,c)(W(b)+W(c))+muli(a,b,c)W(b)W(c)\begin{aligned} F_i(a,b,c)={}&\operatorname{add}_i(a,b,c)\bigl(W(b)+W(c)\bigr) \\ &+\operatorname{mul}_i(a,b,c)\,W(b)\,W(c) \end{aligned}
Sumcheck proves the claimed layer relation without the verifier summing every gate assignment itself.

06 · Concrete test

The test circuit makes the layer claims inspectable

The repository test begins with inputs [1,2,3,4,5,6,7,8]. The first computed layer contains one addition and three multiplications, producing [3,12,30,56]. The next produces [15,1680], and the final addition produces 1695.

That tiny circuit is useful because every wire value can be calculated by hand while the GKR code still constructs wiring polynomials, runs a sumcheck proof per layer, evaluates W(b) and W(c), and folds the next claim with transcript-derived alpha and beta.

Layered circuit used by the testtext
inputs        [1, 2, 3, 4, 5, 6, 7, 8]
layer 2       [1+2, 3×4, 5×6, 7×8] = [3, 12, 30, 56]
layer 1       [3+12, 30×56]          = [15, 1680]
output        [15+1680]              = [1695]

07 · Engineering notes

What is implemented - and what still separates it from a proof system

The repository contains real prover and verifier reductions, but several pieces remain educational. The basic verifier receives the full polynomial. GKR and circuit representation use cloning and dense evaluation tables. Debug output lives on hot paths. Transcript framing needs hardening, and there is no commitment binding the verifier to a large private witness or oracle.

The next useful work is not to add a polished proof-byte serializer. It is to strengthen invariants: mutate every round polynomial and confirm rejection; vary circuit depth and non-power-of-two shapes; domain-separate every transcript; measure prover field operations; and place a polynomial commitment at the boundary where the verifier currently receives a full object.

That progression is why implementing protocols is valuable. It exposes exactly where algebra ends and cryptographic binding begins - and it makes the remaining gaps impossible to mistake for completed security.

  • Add adversarial and property-based tests for proof mutation
  • Harden transcript framing and domain separation
  • Replace dense clones with clearer ownership and streaming folds
  • Bind large polynomial claims with an explicit commitment layer
  • Document complexity from measured field operations, not only asymptotics