Skip to content

Witness Column lookup - #228

Open
mariari wants to merge 16 commits into
NethermindEth:main-beta-lookupfrom
mariari:mariari/pointer-query
Open

Witness Column lookup#228
mariari wants to merge 16 commits into
NethermindEth:main-beta-lookupfrom
mariari:mariari/pointer-query

Conversation

@mariari

@mariari mariari commented Jul 30, 2026

Copy link
Copy Markdown

Follows the design found here:
https://github.com/NethermindEth/zinc-plus/blob/main-beta-lookup/documentation/lookup-methods-design.md

This work is based off of #main-beta-lookup, which may have issues, I'm providing this patch mainly to demonstrate what features that are required to properly have pointers in witness data be chased without manually encoding it for the verifier

Adds a lookup whose table is a committed witness column: ComposedReadSpec { value_col, bit_cols, result_col }declares R(x) = V(b(x)) over the cube. Discharged algebraically (M0 shape) — two chained sumchecks anchored at r*, nothing extra committed, two extra int openings pending a reducer. Details, transcript order, and soundness preconditions in documentation/pointer-query-design.md.

Tested e2e with tamper-rejects (PointerHopUair and its forged twin); existing suite unaffected.

mariari and others added 11 commits July 18, 2026 16:23
This changes the trace length from 2,048 to 32,768
The floating branch = "main" dependency broke: crypto-primitives main
now wants crypto-bigint >=0.7.1 while the workspace pins =0.7.0-rc.9,
and rev 234f101 moved cfg() behind HasPrimeFieldConfig. 9ce6d87 is the
newest rev this tree compiles against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten agent worktrees were committed as submodule entries with no
.gitmodules URL. cargo cannot fetch this repo as a git dependency
while they remain: it tries to init every submodule and fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On main-beta the combined lane preserves the true residue of an
assert_zero constraint (CombinedPolyRowBuilder::assert_zero), while
step 2 skips zero-ideal constraints assuming the claim is zero by
construction. The CPR then adopts the claimed values as its expected
sums, so nothing ever asks them to vanish: an honest prover run on a
false witness verified. All-zero-ideal UAIRs always take this lane;
the MLE_FIRST dispatch ignores zero-ideal constraints when it
classifies.

Step 3 now requires, for all-zero-ideal UAIRs only, that each claimed
value vanish at the projecting element (drawn after the claims are
absorbed). Mixed-ideal UAIRs (SHA/ECDSA) discharge assert_zero through
their own lift semantics and keep their behavior.

Found by tampered-witness examples at this pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verifier-side pointer query of the FOL paper (Lemma 4.2's
C_i(C_j(X)) obligation), designed against the lookup-methods menu:
per-position reads need no multiset argument, so the check is M0
algebra - one extra step-4 group discharges R through the existing
eval cascade, and two chained plain sumchecks push eq-mass through
the pointer map to read V at the committed bit columns' position.
Nothing new is committed; stage 1 pays two extra int-batch openings,
the same shape GKR-LogUp shipped with before its reducer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ComposedReadSpec carries one pointer query: a value column, the
address-bit columns that spell the cube position (low bit first),
and the result column the dereference lands in. Shape validation at
signature construction, the num_vars check deferred to proving time
where the trace size is known.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PointerHopUair: int columns [V, bits, R], bits on the booleanity
sumcheck, one declared composed read, trivially-satisfied general
constraint - the read carries the soundness work. Its broken twin
forges one result entry off its dereference; the e2e test demands
rejection and fails today, which is the point: nothing checks the
pointer query yet. The design it drives is
documentation/pointer-query-design.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sumcheck A reads the value column against the eq-mass pushed through
the pointer map, anchored to the result column's already-discharged
evaluation at the step-4 point; sumcheck B discharges the bridge by
evaluating the eqt product over the committed bit columns. Nothing
committed, nothing new at step 4; the endpoint claims wait for the
protocol layer to open the int batch at the two derived points.
Absence is the protocol layer's to encode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
step4c runs the two chained sumchecks after the lookup step; the
result anchor reads off the resolver's up-evals at the step-4 point,
so R costs nothing. Step 6 lifts the witness-int columns at the two
derived points and absorbs them; step 7 discharges the endpoint
claims with two extra int-batch openings, the shape GKR-LogUp first
shipped with. The proof gains a presence-flagged pointer-query
section; the fold paths carry it as absent. The hop UAIR proves end
to end, the forged dereference rejects from the pointer query
itself, and bridge and lifted-eval tampers reject; the full suite
stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tau collapsed into r* during implementation - the resolver already
discharges every up-eval there, so the step-4 group vanished and the
R side costs nothing. The transcript-order and staging sections now
describe the protocol as wired, not as first sketched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Between turning a column into chunk indices and taking the parent's
lift, a lookup group's proof is a function of the chunk indices and the
subtable alone: the multiplicities, the challenges, both fraction trees
and both GKR runs never ask what the parent was made of. That half is
now prove_fraction_phase, and prove_group calls it.

Nothing changes for BitPoly. The point is the seam: a second table type
needs its own extraction and its own lift, and nothing in between.
Word was declared, transcribed, and given its table, subtable and shift
when GKR-LogUp was ported, and then only BitPoly was implemented. A
Word spec has had no path to a proof since: both the prover's and the
verifier's table match fell through to WitnessNotInTable.

The parents differ and nothing else does. A BitPoly cell is a
polynomial, so its chunks are coefficient blocks, its lift walks D bits,
and the chunks recombine by X^{k·cw}. A Word cell is an integer, so a
chunk is a shift and a mask, the lift is one multilinear evaluation
carried as a degree-0 polynomial, and the chunks recombine by place
value, 2^{k·cw}, which is what word_shift was always for. In between,
the fraction phase counts chunk indices and cannot tell the difference.

An integer reaches its value through ConstTranscribable, the only
integer-shaped thing the trace's cells promise. That decides the
out-of-table case for free: a negative cell transcribes with its high
bits set, reads back above every 2^width, and is refused. A range check
that refuses negatives is the whole point, so it should fall out of the
representation rather than be tested for.
A lookup spec is grouped by its table type, so every column range-checked
at one width and chunk width arrives as a single Word group. That group's
parents are integer columns, counted past the binary and arbitrary ones,
and it proves through prove_group_word.

Its claim is discharged the way the pointer query discharges r_A and r_B:
the witness-int lifted evaluations at the group's own r_inner, absorbed
after the r_0 evals, and one more int-batch opening at that point. The
comment beside the pointer query already calls an int multipoint reducer
the symmetric future fold, and that is as true here -- one opening per
group is stage one, not the end state.

Stage one also takes a single Word group and refuses a second, since one
width and chunk width is what a range check needs and more than one wants
the fold rather than an opening apiece.

The verifier half follows: the lifted evals still have to reach it, which
is a proof field and its transcription, and the parent claim still has to
be checked against them. Until a UAIR declares a Word spec nothing here
runs, and BitPoly is untouched.
The proof carries the witness-int lifted evaluations at the Word group's
r_inner, the verifier binds the group's parent claim against them, and
the extra int opening at that point is checked. Without the binding the
lookup would prove a range for numbers unrelated to the committed
columns, so that check is the whole of what makes this sound.

Three asymmetries had to be fixed to get there, each found by the
end-to-end test rather than by reading:

The bin reducer keyed off the number of lookup groups, counting a Word
group that has no bin lifts to reduce. It now counts binary groups, on
both sides, and the verifier also allows for an empty bin batch, which
the prover already did by way of its absent commitment hint.

The r_inners handed to that reducer included the Word group's, so the
prover offered a bin claim with empty lifts while the verifier had
already dropped it. Both now pass only the binary groups' points.

The prover absorbed the word lifts into the transcript and the verifier
did not, so every challenge after that point diverged and the opening
failed its proximity check. The verifier now absorbs them in the same
place, before the pointer query's.

IntWordLookupUair is four integer columns declared as one Word{16,8}
group with a trivial constraint, so proving it is exactly the claim that
every cell lies in [0, 2^16). Its test proves, serializes, deserializes
and verifies; the companion test corrupts a lifted evaluation and
requires the verification to fail.
The witness fraction tree is built over num_chunks * witness_len leaves.
Both paths only ever build power-of-two trees -- BitPoly because a table
of width D chunked to divide it always gives a power-of-two count, and a
trace is a power of two either way -- so the padded branch that exists
for the other case has never run. Word takes an arbitrary width, which
made that branch reachable, and it does not hold: a chunk count of five
proved and then failed verification.

Refusing the shape is the honest fix. The padding is untested rather than
supported, and a range check needs no width whose chunking is awkward:
20 bits in chunks of five is four chunks, and that is the whole demand.
The refusal names the count, where a verifier rejection named nothing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant