Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d5ff026
feat(recursion): scaffold gadget primitives for recursive verifier
ivokub May 27, 2026
8f1ce6b
feat(recursion): add FRI fold and batching gadgets
ivokub May 27, 2026
5e4ef33
feat(recursion): wire FRI query chain + add bits and binexp gadgets
ivokub May 27, 2026
f75f57c
feat(recursion): compose bits+binexp into friround and add idxselect
ivokub May 27, 2026
d91b4e5
feat(recursion): wire end-to-end FRI query verifier (rounds + chain +…
ivokub May 27, 2026
212ee04
feat(recursion): wire level-batching gamma-mix into frichain
ivokub May 27, 2026
60a6b4f
feat(recursion): pin alpha and gamma constant across module rows
ivokub May 27, 2026
8b63d34
feat(recursion): add width-24 Poseidon2 gadget (sponge variant)
ivokub May 27, 2026
79a8ae7
feat(recursion): add Merkle leaf-hash and node-hash gadgets
ivokub May 27, 2026
efba2f3
feat(recursion): close hash-binding gap in merkle path traversal
ivokub May 27, 2026
0832219
feat(recursion): bind merkle row 0 to leafhash digest
ivokub May 27, 2026
77445a1
test(recursion): end-to-end FRI verifier with Merkle binding
ivokub May 27, 2026
55c6687
fix(board): populate Module field in AddExposeIthValueStep ctx
ivokub May 28, 2026
29dbce0
feat(recursion): close cross-module gap via exposed values
ivokub May 28, 2026
a22968c
feat(recursion): add AIR-at-zeta DAG evaluator gadget
ivokub May 28, 2026
159a3cc
feat(recursion): full AIR-at-zeta check gadget
ivokub May 28, 2026
8f4167a
feat(recursion): add DEEP-quotient bridge gadget primitives
ivokub May 28, 2026
ddf4468
feat(recursion): add width-24 Fiat-Shamir challenger gadget
ivokub May 28, 2026
ef95b9b
feat(recursion): Stage-1 buildVerifierCore — AIR-only
ivokub May 28, 2026
72d2669
feat(recursion): Stage-2 buildVerifierCore — PublicInput, Exposed, Fi…
ivokub May 28, 2026
83692f9
feat(recursion): Stage-3 buildVerifierCore — in-circuit zeta via FS c…
ivokub May 28, 2026
4004d75
feat(recursion): Stage-4 buildVerifierCore — DEEP_ALPHA in FS chain
ivokub May 28, 2026
d12665e
feat(recursion): Stage-5 buildVerifierCore — DEEP_ALPHA bindings as w…
ivokub May 28, 2026
ca26bf4
feat(recursion): Stage-6 buildVerifierCore — extend FS chain through …
ivokub May 28, 2026
fc42299
feat(recursion): Stage-7 buildVerifierCore — full FS chain through al…
ivokub May 28, 2026
2a74994
test(recursion): non-SkipFRI buildVerifierCore on Fibonacci inner
ivokub May 28, 2026
6ee035d
recursion: export BuildVerifierCore + add 5-stage benchmark
ivokub May 28, 2026
7bbbed3
perf(recursion): cut BuildVerifierCore by 600x via two fixes
ivokub May 28, 2026
2b40710
feat(recursion/bits): RegisterAt — sparse-row bit decomposition
ivokub May 28, 2026
b9bbbc0
feat(recursion): Stage 8 — in-circuit FRI final-poly match
ivokub May 28, 2026
67312ec
feat(recursion): Stage 9 — cross-round FRI fold chain
ivokub May 28, 2026
7da410c
feat(recursion): Stage 10 — per-layer Merkle openings for query 0
ivokub May 28, 2026
e83c1ae
feat(recursion): Stage 11 — DEEP-quotient bridge (single-size)
ivokub May 28, 2026
339090d
feat(recursion): Stage 12 — per-column Merkle openings
ivokub May 28, 2026
2a54b9f
feat(recursion): multi-block flexible leafhash + Stage 12 covers all …
ivokub May 28, 2026
a6d2c36
feat(recursion): Stage 13 — multi-degree DEEP bridge
ivokub May 28, 2026
ce33e7a
feat(recursion): per-column Merkle openings for multi-degree
ivokub May 28, 2026
b4a0a2b
feat(recursion): Stage 14 — per-level Merkle openings
ivokub May 28, 2026
9e4f12f
feat(recursion): Stage 15 — multi-degree FRI fold chain
ivokub May 28, 2026
4d49007
feat(recursion): Stage 10 extended to all FRI queries
ivokub May 28, 2026
5aae13b
refactor(recursion): generalise finalPoly lookup to arbitrary 2^k
ivokub May 29, 2026
47bef00
feat(recursion): aggregation API + tree-aggregation driver
ivokub May 29, 2026
29553c0
Merge remote-tracking branch 'origin/main' into feat/recursion-2
ivokub May 29, 2026
0276e28
feat: migrate to E6
ivokub Jun 2, 2026
49fbc46
chore: more E6 renaming
ivokub Jun 2, 2026
1d2ab15
chore: use mustsetrandom
ivokub Jun 2, 2026
2e188bb
fix: set all limbs
ivokub Jun 2, 2026
5313ae9
fix: remove tautological check
ivokub Jun 2, 2026
acdd278
test: bench with fri
ivokub Jun 2, 2026
3e849e3
fix: use MustSetRandom
ivokub Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion board/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func (b *Builder) addExposeRelativeIthValuePublicConstraint(module string, E exp
// AddExposeIthValueStep adds a constraint Lagrange_pos * (expr - expr[pos]), and stores expr[pos] in the proof so the verifier has access to it
// the 1 entry column expr[pos] is registered in the trace
func (b *Builder) AddExposeIthValueStep(module string, E expr.Expr, out string, pos int) {
ctx := ExposeIthValueCtx{Pos: pos}
ctx := ExposeIthValueCtx{Module: module, Pos: pos}
pvStep := ProverStep{
Ctx: ctx,
Ins: []expr.Expr{E},
Expand Down
8 changes: 5 additions & 3 deletions expr/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,11 @@ func (l *Leaf) Add(e Expr) Expr { return &Add{l, e} }
func (l *Leaf) Sub(e Expr) Expr { return &Sub{l, e} }
func (l *Leaf) Mul(e Expr) Expr { return &Mul{l, e} }
func (l *Leaf) Pow(n uint32) Expr {
if n > 2 {
return squareAndMultiply(l, n)
}
// Leaves are atomic — pruneSearch (the in-place rewriter that the
// squareAndMultiply Clone-cascade was guarding against) explicitly
// skips Leaf nodes (see ast.go: pruneSearch case *Leaf). So we can
// safely return a unary Pow{leaf, n} regardless of n and avoid the
// O(n^2) Mul-tree expansion that bites large expr.Fold sums.
return &Pow{l, n}
}

Expand Down
71 changes: 71 additions & 0 deletions recursion/aggregation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright Consensys Software Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package recursion

import (
"fmt"

"github.com/consensys/loom/board"
"github.com/consensys/loom/trace"
)

// BuildAggregationCore compiles a board.Program that verifies BOTH inner
// proofs in a single outer circuit. The two sub-verifiers are wired into
// the same builder under disjoint module-name prefixes ("L_" and "R_") so
// they share no columns or modules, but a single board.Compile + outer
// prove/verify covers them at once — yielding one aggregated proof for
// the pair.
//
// This is the foundation of tree-aggregation: each non-leaf node of the
// aggregation tree is a BuildAggregationCore circuit verifying the two
// child proofs from the level below; AggregateProofs drives that loop.
//
// The Left and Right halves are independent — programs may differ in
// shape, sizes, and hash backends (though both must be Poseidon2, per
// validateInnerProof). Per-half customisation through cfg.ModulePrefix
// is ignored here; this function manages the prefixes itself.
func BuildAggregationCore(input AggregationInput, cfg Config) (board.Program, trace.Trace, error) {
if err := validateInnerProof(input.Left.Proof, cfg); err != nil {
return board.Program{}, trace.Trace{}, fmt.Errorf("aggregation: left: %w", err)
}
if err := validateInnerProof(input.Right.Proof, cfg); err != nil {
return board.Program{}, trace.Trace{}, fmt.Errorf("aggregation: right: %w", err)
}

leftCfg := cfg
leftCfg.ModulePrefix = "L_"
rightCfg := cfg
rightCfg.ModulePrefix = "R_"

builder := board.NewBuilder()
leftTrace, err := buildVerifierCoreInto(&builder, input.Left, leftCfg)
if err != nil {
return board.Program{}, trace.Trace{}, fmt.Errorf("aggregation: build left: %w", err)
}
rightTrace, err := buildVerifierCoreInto(&builder, input.Right, rightCfg)
if err != nil {
return board.Program{}, trace.Trace{}, fmt.Errorf("aggregation: build right: %w", err)
}

merged, err := trace.MergeMatching(leftTrace, rightTrace)
if err != nil {
return board.Program{}, trace.Trace{}, fmt.Errorf("aggregation: merge traces: %w", err)
}

pg, err := board.Compile(&builder)
if err != nil {
return board.Program{}, trace.Trace{}, fmt.Errorf("aggregation: compile: %w", err)
}
return pg, merged, nil
}
133 changes: 133 additions & 0 deletions recursion/aggregation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright Consensys Software Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package recursion

import (
"testing"

"github.com/consensys/loom/prover"
"github.com/consensys/loom/setup"
"github.com/consensys/loom/verifier"
)

// TestBuildAggregationCorePair builds an aggregation circuit verifying
// two distinct Fibonacci(n=4) inner proofs and confirms the outer
// proof passes prove + verify end-to-end. Both halves use SkipFRI so
// the test runs in reasonable wall time.
func TestBuildAggregationCorePair(t *testing.T) {
leftProg, leftTrace := makeFibonacciInner(t, 4)
leftProof, err := prover.Prove(leftTrace, setup.ProvingKey{}, nil, leftProg, prover.SkipFRI())
if err != nil {
t.Fatalf("left inner prove: %v", err)
}
rightProg, rightTrace := makeEqualityInner(t, 4)
rightProof, err := prover.Prove(rightTrace, setup.ProvingKey{}, nil, rightProg, prover.SkipFRI())
if err != nil {
t.Fatalf("right inner prove: %v", err)
}

outerProg, outerTrace, err := BuildAggregationCore(
AggregationInput{
Left: RecursionInput{Program: leftProg, Proof: leftProof},
Right: RecursionInput{Program: rightProg, Proof: rightProof},
},
DefaultConfig(),
)
if err != nil {
t.Fatalf("BuildAggregationCore: %v", err)
}
outerProof, err := prover.Prove(outerTrace, setup.ProvingKey{}, nil, outerProg, prover.SkipFRI())
if err != nil {
t.Fatalf("aggregated prove: %v", err)
}
if err := verifier.Verify(nil, setup.VerificationKey{}, outerProg, outerProof, verifier.SkipFRI()); err != nil {
t.Fatalf("aggregated verify: %v", err)
}
}

// TestBuildAggregationCoreRejectsBadLeft tampers the left inner proof
// (its AT-zeta values) and confirms the aggregated outer verifier
// rejects.
func TestBuildAggregationCoreRejectsBadLeft(t *testing.T) {
leftProg, leftTrace := makeFibonacciInner(t, 4)
leftProof, err := prover.Prove(leftTrace, setup.ProvingKey{}, nil, leftProg, prover.SkipFRI())
if err != nil {
t.Fatalf("left inner prove: %v", err)
}
rightProg, rightTrace := makeEqualityInner(t, 4)
rightProof, err := prover.Prove(rightTrace, setup.ProvingKey{}, nil, rightProg, prover.SkipFRI())
if err != nil {
t.Fatalf("right inner prove: %v", err)
}

// Tamper left's A value at zeta.
a, ok := leftProof.ValueAtZetaExt("A")
if !ok {
t.Fatal("A not in left.ValuesAtZeta")
}
a.B0.A0.SetUint64(a.B0.A0.Uint64() + 1)
leftProof.SetValueAtZetaExt("A", a)

outerProg, outerTrace, err := BuildAggregationCore(
AggregationInput{
Left: RecursionInput{Program: leftProg, Proof: leftProof},
Right: RecursionInput{Program: rightProg, Proof: rightProof},
},
DefaultConfig(),
)
if err != nil {
t.Fatalf("BuildAggregationCore: %v", err)
}
outerProof, err := prover.Prove(outerTrace, setup.ProvingKey{}, nil, outerProg, prover.SkipFRI())
if err != nil {
return
}
if err := verifier.Verify(nil, setup.VerificationKey{}, outerProg, outerProof, verifier.SkipFRI()); err == nil {
t.Fatalf("aggregated verify accepted tampered left inner proof")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tamper test exits on prove

Low Severity

TestBuildAggregationCoreRejectsBadLeft returns without failing when prover.Prove errors after tampering the left proof. The test title expects rejection at verify time; a silent pass hides regressions where prove fails for unrelated reasons or where a broken verifier never reaches Verify.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e849e3. Configure here.

}

// TestAggregateInputsTwoLeaves drives the tree-aggregation loop with
// two leaf proofs (one root aggregation step).
//
// Currently skipped under testing.Short() because building the root
// aggregation circuit requires running BuildVerifierCore on a leaf
// VERIFIER's airverify program — itself a multi-module program with
// large constraint expressions. board.Compile times out on the
// current implementation. Performance work (sharing merkle modules
// across queries, flattening constraint expressions) is the next
// stage before tree aggregation becomes test-affordable.
func TestAggregateInputsTwoLeaves(t *testing.T) {
if testing.Short() {
t.Skip("aggregation tree compile is slow; run with -short=false")
}
t.Skip("TODO: optimise aggregation circuit compile before re-enabling — see commit message")
inputs := make([]RecursionInput, 2)
for i := range inputs {
prog, tr := makeFibonacciInner(t, 4)
prf, err := prover.Prove(tr, setup.ProvingKey{}, nil, prog, prover.SkipFRI())
if err != nil {
t.Fatalf("leaf %d inner prove: %v", i, err)
}
inputs[i] = RecursionInput{Program: prog, Proof: prf}
}

rootProg, rootProof, err := AggregateInputs(inputs, DefaultConfig())
if err != nil {
t.Fatalf("AggregateInputs: %v", err)
}
if err := verifier.Verify(nil, setup.VerificationKey{}, rootProg, rootProof, verifier.SkipFRI()); err != nil {
t.Fatalf("aggregated root verify: %v", err)
}
}
57 changes: 57 additions & 0 deletions recursion/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright Consensys Software Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package recursion

import (
"fmt"

"github.com/consensys/loom/internal/commitment"
"github.com/consensys/loom/proof"
)

// Config carries optional knobs for verifier-circuit construction. Defaults
// are exposed via DefaultConfig.
type Config struct {
// HashBackendID identifies the algebraic hash the inner proof was produced
// with. Only Poseidon2 is supported for recursion.
HashBackendID string

// ModulePrefix is prepended to every module / column name BuildVerifierCore
// emits. Use it when composing multiple verifier circuits into the same
// outer builder (e.g. BuildAggregationCore wires two inner proofs by
// instantiating BuildVerifierCore twice with distinct prefixes). The empty
// prefix is the default and matches the historical single-proof layout.
ModulePrefix string
}

// DefaultConfig returns a Config preset for Poseidon2 recursion, which is the
// only mode currently supported.
func DefaultConfig() Config {
return Config{HashBackendID: commitment.HashBackendPoseidon2}
}

// validateInnerProof ensures the inner proof is recursion-compatible. SHA-256
// proofs cannot be recursed because their hash chain has no efficient
// in-circuit encoding.
func validateInnerProof(p proof.Proof, cfg Config) error {
id := commitment.NormalizeHashBackendID(p.HashBackendID)
want := commitment.NormalizeHashBackendID(cfg.HashBackendID)
if id != commitment.HashBackendPoseidon2 {
return fmt.Errorf("recursion: inner proof uses hash backend %q; only %q is supported", id, commitment.HashBackendPoseidon2)
}
if want != commitment.HashBackendPoseidon2 {
return fmt.Errorf("recursion: config hash backend %q is not supported (only %q)", want, commitment.HashBackendPoseidon2)
}
return nil
}
38 changes: 38 additions & 0 deletions recursion/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright Consensys Software Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

// Package recursion builds Loom programs that verify other Loom proofs.
//
// Two top-level entry points are planned:
//
// - BuildVerifierCore compiles a verifier circuit for a single inner proof
// (used for proof compression by repeated wrapping).
// - buildAggregationCore compiles a verifier circuit that verifies two
// inner proofs at once (used for tree-based aggregation of segmented
// traces).
//
// Recursion is only meaningful with an algebraic hash, so the inner proof
// must use the Poseidon2 backend.
//
// This milestone delivers only the primitive gadget layer:
//
// - gadgets/poseidon2 in-circuit Poseidon2 permutation (width-16 MD variant)
// - gadgets/merkle Merkle-path verification on top of the Poseidon2 gadget
// - gadgets/challenger Fiat-Shamir sponge layered over the Poseidon2 gadget
// - extfield E6 arithmetic helpers inlined as expr.Expr trees
//
// The BuildVerifierCore / buildAggregationCore wiring is intentionally left as
// stubs returning an error; subsequent milestones will assemble these gadgets
// into a complete verifier circuit (AIR-at-zeta check, FRI verification,
// Merkle-opening verification, DEEP-quotient bridge).
package recursion
Loading