look ↔ BBG_root: putting the state root in the Statement
Status: design note — written from the bbg side (soft3-release M6). The bbg-side prerequisites are DONE; everything below the "what zheng must add" line is zheng-side work, not yet implemented.
the gap
build_look_steps_from_trace (ccs/mod.rs) already binds each look opening all
the way up to the root: opening soundness → value = r[7] → point = corner of
r[6] → commitment = leaves.dims[r[5]] → root_from_leaves(leaves) = trace
registers r[4], r[11], r[12], r[13]. But the root registers come from the
program's object — witness data. Nothing binds them to a public value, so a
prover can fabricate a self-consistent state (any leaves, any root), put its
root in the object, and every binding holds. The residual in
axis-verifier-integration.md ("look (17): BBG_root in Statement") is exactly
this missing final link: the root must be a public input.
what bbg now provides (all landed, nothing else needed from bbg)
BbgState::root() -> Particle— the BBG_root as[u8; 32]: four Goldilocks limbs, little-endian, packed limb i at bytes[8i, 8i+8). This is the exact packing ofzheng::root_from_leavesoutput (seebbg/rs/src/state.rs::compute_root).BbgState::root_leaves() -> zheng::RootLeaves— the 14-leaf preimage, structurally identical to what the in-circuit replay recomputes.- serde (new, M6):
bbg::QueryProof,lens::Commitment,lens::OpeningSerialize/Deserialize behind aserdecargo feature in both crates, wire format pinned by golden fixtures. Docs:bbg/docs/api/query-proof-wire.md. A statement's root field needs no special serde — it is a plain[u8; 32]likeprogram_hash.
what zheng must add
Statement.bbg_root: [u8; 32](types.rs). Use thecompute_rootpacking above;[0u8; 32]for programs with no look rows (no real state has the zero root — it is the "no state read" sentinel). AnOption<[u8; 32]>also works but breaks Statement's field uniformity.- Fiat-Shamir: absorb it in
Transcript::absorb_statement(transcript.rs) afterfocus_bound, so prover and verifier bind the same root. This changes every proof's transcript — coordinate with the accumulator-size stabilisation (soft3 blocker 3) so the format breaks once, not twice. - Verifier check (
verifyin lib.rs, or a step emitted bybuild_look_steps_from_trace's verifier-side twin): for everyLookOpening,root_from_leaves(&lo.leaves)packed to bytes must equalstatement.bbg_root. Equivalently as limbs:root[i].as_u64().to_le_bytes() == statement.bbg_root[8i..8i+8]. With that, the existing chain (root = r[4]/r[11]/r[12]/r[13] = object limbs) closes end-to-end: object root = recomputed root = public root. commit(): reject (CommitError::LookBinding) if any opening's recomputed root disagrees withstatement.bbg_root— same refuse-to-emit policy the other look bindings already follow. Notecommit()does not currently take aStatement; either pass it in or check indecide/verifyonly.
how look bindings consume it (caller side)
The bbg/nox caller flow becomes:
let root = state.root; // bbg — public input
let statement = Statement ;
// program object carries goldilocks_from_bytes32(&root) limbs (unchanged)
let provider = new; // bbg — records openings
let outcome = reduce;
let openings = provider.take_look_openings; // leaves = state.root_leaves()
let proof = commit?;
verify?; // now checks root publicly
bbg/rs/tests/look_e2e.rs is the template; once Statement carries the root,
its "declare root in object" step stops being the only root anchor.
non-goals
- No change to
RootLeaves,root_from_leaves, or the register conventions — they already match bbg exactly. - No bbg-side change: root exposure and wire serde are complete.
- Sizing: +32 bytes per Statement, one extra absorb, ≤ a few eq-steps per
proof. No new CCS pattern;
pattern_look_inlinestays trivial.