diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index e140ca0ad528..d0814a268648 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -98,6 +98,7 @@ add_subdirectory(barretenberg/flavor) add_subdirectory(barretenberg/goblin) add_subdirectory(barretenberg/goblin_avm) add_subdirectory(barretenberg/grumpkin_srs_gen) +add_subdirectory(barretenberg/msm_verification) add_subdirectory(barretenberg/multilinear_batching) add_subdirectory(barretenberg/numeric) add_subdirectory(barretenberg/op_queue) diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/msm_verification/CMakeLists.txt new file mode 100644 index 000000000000..24f8becb3e11 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/CMakeLists.txt @@ -0,0 +1 @@ +barretenberg_module(msm_verification common transcript ecc numeric crypto_merkle_tree) diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/.gitignore b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/.gitignore new file mode 100644 index 000000000000..508d64bf3015 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/.gitignore @@ -0,0 +1 @@ +ecfft_domain_2_*.bin diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/NATIVE_OPTIMIZATIONS.md b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/NATIVE_OPTIMIZATIONS.md new file mode 100644 index 000000000000..1de9ba22e9b3 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/NATIVE_OPTIMIZATIONS.md @@ -0,0 +1,206 @@ +# BaseFold Native Proof Size: Optimization Analysis + +This document analyzes the native proof size of the BaseFold protocol and +identifies concrete optimizations to reduce it. + +## Baseline + +Parameters: 2^15 MSM over Grumpkin, blowup factor 8, domain size 2^18, +18 fold rounds, 43 queries (~128-bit security). + +### Where the bytes go + +``` +Fixed: + - 18 Merkle roots: 18 × 32 = 576 bytes + - 1 final group element (x, y): 2 × 32 = 64 bytes + Fixed total: 640 bytes + +Per query, per round r (oracle size 2^{18-r}, Merkle depth = 18-r): + - 2 group element openings (P_r, Q_r): 2 × 64 = 128 bytes + - 2 Merkle sibling paths: 2 × (18-r) × 32 bytes + - 1 fold result (F_r): 64 = 64 bytes + Per round subtotal: 192 + 64·(18-r) bytes + +Per query total = Σ_{r=0}^{17} [192 + 64·(18-r)] + = 18 × 192 + 64 × (18 + 17 + ... + 1) + = 3,456 + 64 × 171 + = 14,400 bytes + +43 queries × 14,400 = 619,200 bytes +Grand total: 640 + 619,200 = 619,840 bytes ≈ 605 KiB +``` + +### Breakdown by category + +| Category | Size | Share | +|---------------------|----------|-------| +| Merkle paths | 460 KiB | 74% | +| Group elements | 148 KiB | 24% | +| Fixed (roots+final) | 0.6 KiB | <1% | +| **Total** | **605 KiB** | 100% | + +Merkle paths dominate at 74% of the proof. + +--- + +## Optimization 1: eliminate redundant fold results (-48 KiB) + +The fold result F_r at pair index j in round r becomes oracle[r+1][j], which the +verifier already opens as one of the pair elements at round r+1. Specifically, +when the query traces from round r to round r+1, the fold output at index j is +opened as P_{r+1} or Q_{r+1} in the next round's Merkle opening. + +So F_r need not be sent separately — it is already present in the proof as an +opened element of round r+1. The verifier just needs to know which element of +the next pair corresponds to the fold result (determined by the query index trace). + +``` +Savings: 43 queries × 18 rounds × 64 bytes = 49,536 bytes ≈ 48 KiB +``` + +**Result: ~557 KiB.** + +This is a pure protocol simplification with no security impact. + +--- + +## Optimization 2: paired Merkle paths (-254 KiB) + +The two openings per round are at indices j and j + half, which are **siblings +at the bottom level** of the Merkle tree (they share the same parent). Their +Merkle paths of depth d therefore share d-1 sibling nodes. + +The verifier knows both leaf values (P_r, Q_r), so it can: +1. Hash both leaves: h_left = hash(P_r), h_right = hash(Q_r) +2. Compute the parent: parent = hash(h_left, h_right) +3. Walk the common path from parent to root using d-1 siblings + +Instead of sending 2 × d siblings, the prover sends only d-1 siblings. + +``` +Current Merkle per query: Σ_{r=0}^{17} 2·(18-r)·32 = 10,944 bytes +Optimized per query: Σ_{r=0}^{17} (18-r-1)·32 = 32 × (17+16+...+0) = 4,896 bytes +Savings per query: 10,944 - 4,896 = 6,048 bytes +Total savings: 43 × 6,048 = 260,064 bytes ≈ 254 KiB +``` + +**Result: ~303 KiB** (after Opt 1+2). + +This works because the protocol always opens pairs — it never opens a single +element in isolation. + +Note: this also saves circuit gates in the recursive verifier (~16% savings, +see OPTIMIZATIONS.md). + +--- + +## Optimization 3: x-only group elements (-50 KiB) + +Each Grumpkin affine point is (x, y) = 64 bytes. If the Merkle tree commits +to hash(x) instead of hash(x, y), the prover only needs to send x (32 bytes) +plus a sign bit for y. The verifier recovers y from the curve equation: + +``` +y² = x³ + b (b is the Grumpkin curve constant) +y = ±√(x³ + b) (sign bit disambiguates) +``` + +In the native verifier this is a field square root + comparison (cheap). In the +recursive verifier, it's a constraint y² = x³ + b (3 gates — essentially free). + +After Opt 1 (eliminating F_r), each query opens 2 group elements per round: + +``` +Current: 43 × 18 × 2 × 64 = 98,304 bytes ≈ 96 KiB +X-only: 43 × 18 × 2 × 33 = 51,084 bytes ≈ 50 KiB (32 bytes + 1 sign bit, rounded up) +Savings: ≈ 46 KiB +``` + +**Result: ~257 KiB** (after Opt 1+2+3). + +Gate count impact: negligible. The on-curve check (y² = x³ + b) costs 3 gates, +and Poseidon2(x) costs the same 73 gates as Poseidon2(x, y) (both fit in one +permutation). The circuit cost benchmark confirmed hash(x,y) and hash(x-only) +give essentially identical gate counts. + +--- + +## Optimization 4: batch Merkle opening (FRI-style, -50+ KiB) + +When multiple queries open the same Merkle tree, their paths share upper-level +siblings. A batch opening proof (as used in Plonky2 and standard FRI +implementations) deduplicates these shared nodes. + +For q queries opening 2q leaves in a depth-d tree, the batch proof sends only +the minimal set of tree nodes needed to reconstruct all q roots: + +``` +Worst case (no sharing): 2q × d nodes +Best case (full sharing): 2q + d nodes +Expected for random queries: roughly 2q × d - (overlap savings) +``` + +For 86 leaves (43 queries × 2) in a depth-18 tree, the expected overlap at +level k is: + +``` +Probability two paths share a level-k node ≈ 86 / 2^{18-k} +Significant sharing begins at level k ≈ 18 - log2(86) ≈ 11 +Levels 0-10: essentially no sharing (all 86 nodes unique) +Levels 11-18: increasing sharing, saving ~1 node per level per collision +``` + +Estimated savings: ~30% of Merkle path data in early rounds (large trees), +less in later rounds (trees are already small). + +``` +Estimated total Merkle after batch opening: ~140 KiB (vs ~210 KiB with Opt 2) +Additional savings: ~70 KiB +``` + +**Result: ~190 KiB** (after Opt 1+2+3+4). + +This is more complex to implement (requires a batch Merkle proof structure) +but is well-understood from FRI implementations. + +--- + +## Optimization 5: increase blowup factor + +Blowup 16 (= 2^4) gives 4 bits of security per query, requiring only +128/4 = 32 queries instead of 43. Trade-offs: + +- Domain grows from 2^18 to 2^19 (one more fold round) +- Prover does 2× more work (larger oracle to commit) +- Query-proportional proof data shrinks by 32/43 ≈ 74% + +``` +Proof size with blowup 16 (after Opt 1+2+3): 32/43 × query_data + fixed + ≈ 0.74 × (257 - 0.6) + 0.6 ≈ 190 KiB +``` + +**Result: ~190 KiB** (after Opt 1+2+3+5, comparable to Opt 4). + +Blowup 32 (= 2^5): 26 queries, domain 2^20. Further reduction to ~155 KiB +but prover overhead grows 4× and the domain precomputation becomes heavier. + +--- + +## Summary + +| Configuration | Proof size | Savings from baseline | +|-----------------------------------------------|------------|----------------------| +| Baseline (blowup 8, 43 queries) | **605 KiB** | — | +| + Opt 1: remove redundant F_r | 557 KiB | 48 KiB | +| + Opt 2: paired Merkle paths | 303 KiB | 302 KiB (cumulative) | +| + Opt 3: x-only group elements | 257 KiB | 348 KiB | +| + Opt 4: batch Merkle opening | ~190 KiB | ~415 KiB | +| + Opt 5: blowup 16 (32 queries) | ~150 KiB | ~455 KiB | + +Opts 1 and 2 are easy to implement and give the biggest bang (605 → 303 KiB). +Opt 3 is straightforward. Opt 4 requires more implementation effort but is +standard FRI machinery. Opt 5 is a parameter choice with prover cost trade-offs. + +All optimizations are compatible with each other and with the recursive verifier +circuit optimizations described in OPTIMIZATIONS.md. diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/OPTIMIZATIONS.md b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/OPTIMIZATIONS.md new file mode 100644 index 000000000000..94e2784eee43 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/OPTIMIZATIONS.md @@ -0,0 +1,167 @@ +# BaseFold Recursive Verifier: Cost Analysis + +This document records the circuit cost of the BaseFold recursive verifier +and analyzes potential optimizations. + +## Concrete measurement + +Parameters: 2^15 MSM over Grumpkin, blowup factor 8 = 2^3, domain size 2^18, +18 fold rounds, 43 queries (~128-bit security). + +Measured by `basefold_circuit_cost.test.cpp::FullSizeRecursiveVerifier`, which +builds the actual `RecursiveBaseFoldVerifier` circuit: + +| Metric | Value | +|-------------------------------|---------------| +| **Total gates** | **4,600,813** | +| **Log2(gates)** | **22.13** | +| Gates per query | 106,995 | +| Gates per query per round | 5,944 | +| Native proof size | 605 KiB | +| Raw batch_mul MSM (comparison)| ~12M gates | +| **Improvement over raw MSM** | **~2.6×** | + +Note: the Merkle path verification uses `conditional_assign` to compute BOTH +hash orderings at each level (needed for a fixed circuit — see PROBLEMS.md). +This doubles the Merkle hashing cost compared to a branching implementation. + +### Cost breakdown per query per round + +Each round for each query does: +1. **Fold check**: 4 group operations (3 constant-scalar muls + 1 witness-scalar mul) +2. **Merkle verification**: 2 paths, each of depth = (18 - round), with 2× + Poseidon2 hashes per level (both orderings + conditional_assign) + +Average Merkle depth across 18 rounds: (18 + 17 + ... + 1) / 18 = 9.5. +Average Merkle cost per round: 2 paths × (1 leaf hash + 9.5 × 2 path hashes) × ~74 gates ≈ 2,960 gates. +Average fold cost per round: 5,944 - 2,960 ≈ **2,984 gates**. + +### Isolated per-operation costs (for reference only) + +These were measured by constructing each operation in its own fresh circuit. +They significantly overestimate the cost in a real circuit: + +| Component | Gates (isolated) | +|---------------------------------|------------------| +| Fold check, e > 0 (4 ops) | 6,513 | +| Fold check, e = 0 (2 ops) | 5,111 | +| Merkle path, depth 18 | 1,407 | +| Merkle path, depth 1 | 149 | + +--- + +## Potential optimizations (circuit cost) + +### Optimization 1: reduce number of queries (increase blowup) + +**Impact: linear reduction in gates and proof size. Easy to implement.** + +| Blowup | Bits/query | Queries | Rounds | Estimated gates | Proof size | +|--------|-----------|---------|--------|----------------|------------| +| 8 | 3 | 43 | 18 | 4.60M (measured)| 605 KiB | +| 16 | 4 | 32 | 19 | ~3.6M | ~470 KiB | +| 32 | 5 | 26 | 20 | ~3.0M | ~400 KiB | + +Trade-off: larger blowup → fewer queries (cheaper verifier) but larger initial +domain (more prover work, bigger precomputed domain data, one more fold round). +Since prover work is native and one-time, this favors the recursive setting. + +### Optimization 2: single-hash Merkle paths (if witness-dependent topology is OK) + +**Impact: ~1.1M gate savings (~24% of total).** + +The current implementation computes BOTH Poseidon2 hash orderings at each Merkle +level and selects with `conditional_assign`. If the domain lookup issue (see +PROBLEMS.md) is resolved via ROM tables, the Merkle index bits would be proper +circuit witnesses and we could use a single conditional hash instead of two. + +This would bring the gate count back to ~3.5M. + +### Optimization 3: paired Merkle paths + +**Impact: reduces BOTH proof size (-254 KiB) AND gate count (~10-15% savings).** + +The two openings per round are siblings at the bottom level of the Merkle tree. +Instead of 2 independent paths, send 1 common path from the parent to the root. +The verifier hashes both leaves to get the parent, then walks one shared path. + +Saves: (d+1) Poseidon2 calls per round per query (or (d+1)×2 with the current +double-hash approach). + +### Optimization 4: eliminate redundant fold result openings + +**Impact: reduces proof size by ~48 KiB. Small gate savings.** + +The fold result F_r at round r is already opened as one of the pair elements +at round r+1. Removing the redundant send saves 43 × 18 × 64 bytes ≈ 48 KiB. + +--- + +## Potential optimizations (proof size only) + +See NATIVE_OPTIMIZATIONS.md for detailed analysis. Summary: + +| Optimization | Proof size savings | +|---------------------------------------|-------------------| +| Remove redundant F_r | 48 KiB | +| Paired Merkle paths | 254 KiB | +| X-only group elements | 46 KiB | +| Batch Merkle opening (FRI-style) | ~70 KiB | +| Increase blowup (fewer queries) | proportional | + +--- + +## What does NOT help (surprising finding) + +**The α,β reformulation makes things WORSE, not better.** + +The fold formula can be algebraically rewritten as: + +``` +result = G_0 · α + G_1 · β +where α = s_0^{-e} · (s_1 - z) / (s_1 - s_0) + β = s_1^{-e} · (z - s_0) / (s_1 - s_0) +``` + +This looks like it should halve the cost: 2 scalar muls instead of 4. +We benchmarked this (isolated) and found: + +| Formulation | Scalar muls | Field arith | Total gates | +|--------------------|-------------|-------------|-------------| +| Original (4 ops) | 6,513 | 0 | **6,513** | +| α,β (2 muls) | 5,111 | 5,122 | **10,233** | + +The α,β version is **57% more expensive**. The reasons: + +1. **Non-native field arithmetic is expensive.** α and β live in Fq (BN254 + base field), which is non-native in a BN254 circuit. Computing them requires + `bigfield` multiplication (CRT reduction + range checks) at ~2,500 gates per + mul. + +2. **Constant-scalar muls are cheap.** In the original formulation, 3 of the 4 + group operations multiply a witness point by a **constant** scalar. + `cycle_group`'s Straus implementation bakes constant scalars directly into + ROM table entries. + +3. **Witness-scalar muls are expensive.** In the α,β version, both muls use + witness scalars, forcing full variable-base Straus. + +**Takeaway**: in the Grumpkin-in-BN254 circuit, optimizing the number of group +operations at the expense of introducing non-native field arithmetic is a bad +trade. The bottleneck is the non-native field, not the number of group ops. + +--- + +## Notes for integration + +- **SRS generators**: In production, the group elements will come from the Aztec + Grumpkin SRS, loaded via `srs::init_file_crs_factory` / `CommitmentKey`. + +- **ECFFT domain binary**: The log_n=18 domain data (~25 MB) is NOT checked into + git. The test generates it on first run via `ecfft_precompute.py` (~2 min). + +- **Origin tags**: The recursive verifier uses a "native hint" approach to avoid + origin tag conflicts. Needs refactoring for production. + +- **Fixed circuit**: See PROBLEMS.md for remaining witness-dependent topology + issues and their estimated cost to fix (~300K additional gates). diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/PROBLEMS.md b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/PROBLEMS.md new file mode 100644 index 000000000000..5de3322d2479 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/PROBLEMS.md @@ -0,0 +1,101 @@ +# BaseFold: Known Issues for Production + +This prototype works and produces correct gate counts, but several aspects +of the recursive verifier are NOT compatible with a fixed-circuit deployment +(e.g., as part of the Aztec root rollup). This document explains the issues. + +## The core problem: witness-dependent circuit topology + +In production, the recursive verifier must be a **fixed circuit** — its gate +layout is determined at compile time, a verification key is computed once, and +every proof is verified against that same VK. The circuit structure must not +depend on witness values. + +The current BaseFold recursive verifier has **witness-dependent structure** +in several places: + +### 1. Merkle branch direction (FIXED in this PR) + +**Problem**: the hash argument order at each Merkle level depends on `index % 2`, +where `index` is derived from a transcript challenge (witness). + +**Fix**: use `conditional_assign` — compute BOTH hash orderings and select based +on an index-bit witness. This doubles the Merkle hashing cost (~2× Poseidon2 +calls per level) but makes the circuit topology fixed. + +**Status**: Fixed. The current implementation uses `conditional_assign`. + +**Concrete cost**: at blowup 8, this added ~1.1M gates (from 3.47M to 4.60M). +This is the single largest overhead for fixed-circuit compatibility. If the +ROM-based approach (item 2 below) is implemented, the index bits become proper +circuit witnesses and we could potentially use a single conditional hash per +level instead of double-hashing, recovering most of this cost. + +### 2. Domain pair lookup + fold scalar lookup (NOT YET FIXED) + +**Problem**: the fold check reads domain points `s0 = domain[j]`, +`s1 = domain[j + half]`, precomputed `pair_diff_inv[j]`, and fold scalars +`s0^{-e}`, `s1^{-e}` — all indexed by the pair index `j`, which depends on +the query index (a witness value). In the current implementation, these are +read as native constants indexed by the native `j`, making the circuit topology +depend on which `j` was chosen. + +**What's needed**: load all per-pair data into **ROM tables** (or plookup tables) +and index into them with the witness `j`. `cycle_group` already uses ROM tables +internally for Straus MSM, so the infrastructure exists. All five values +(s0, s1, pair_diff_inv, s0^{-e}, s1^{-e}) can share a single ROM per round, +storing one struct per pair. + +**Cost estimate**: ROM table construction costs ~1 gate per entry. At blowup 32 +(the recommended configuration), the domain has 2^20 entries and round 0 has +2^19 pairs. Total ROM construction across all rounds (dominated by round 0): +~2^20 ≈ 1M gates. This is significant — about 30% of the current 3.25M gate +count. However, the read cost is negligible (~5 ROM reads per fold check × +26 queries × 20 rounds ≈ 2,600 gates). + +Note: at smaller blowup (e.g., blowup 8, domain 2^18), the ROM cost is ~260K +gates (~5% of the 4.6M count). The ROM cost scales linearly with domain size, +so higher blowup makes this overhead proportionally larger. This trade-off +may affect the optimal blowup choice for a production deployment. + +### 3. Query index derivation (NOT YET FIXED) + +**Problem**: query indices are computed natively via `Poseidon2::hash(seed, q)` +and used as native `size_t` values for indexing. In a fixed circuit, the +index should be computed in-circuit and decomposed into witness bits. + +**What's needed**: compute the Poseidon2 hash in-circuit (already done for Merkle), +decompose the hash output into bits, and use those bits to select the correct +Merkle path and domain pair. The bits would drive conditional selects and ROM +lookups. + +**Cost estimate**: one stdlib Poseidon2 hash per query (~74 gates) + bit +decomposition (~20 bits for log2(domain_size) = 20). Total: ~100 gates per +query × 26 queries ≈ 2,600 gates. Negligible. + +## Summary + +| Issue | Status | Estimated cost to fix | +|-------|--------|----------------------| +| Merkle branch direction | **Fixed** | ~1.1M gates (already in measurements) | +| Domain pair + fold scalar ROM | Not fixed | ~1M gates at blowup 32, ~260K at blowup 8 | +| Query index derivation | Not fixed | ~2,600 gates (negligible) | + +**Estimated total at blowup 32 with all fixes**: 3.25M (current) + 1M (ROM) ≈ +**4.25M gates**. However, if the ROM enables single-hash Merkle (replacing the +current double-hash `conditional_assign` approach), we'd recover ~1M gates, +netting out to approximately the same ~3.25M. + +**Estimated total at blowup 8 with all fixes**: 4.6M + 260K ≈ 4.86M gates +(with double-hash Merkle), or ~3.75M with single-hash Merkle. + +## Alternative: accept witness-dependent topology + +Some FRI verifier implementations (e.g., Plonky2) accept witness-dependent +circuit topology by design — they generate a fresh circuit for each proof. +This works in settings where: +- The verifier circuit is compiled per-proof (no fixed VK) +- Or the circuit uses a VM/dynamic execution model + +In the Aztec setting with fixed UltraHonk circuits, this is NOT acceptable. +The ROM-based approach described above is the standard solution. diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold.hpp b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold.hpp new file mode 100644 index 000000000000..3428642708f8 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold.hpp @@ -0,0 +1,523 @@ +#pragma once +/** + * @file basefold.hpp + * @brief Group-valued BaseFold: FRI-based MSM verification over Grumpkin. + * + * Reference: "Revisiting the IPA-sumcheck connection", eprint 2025/1325, Section 7. + * + * # Templating on Curve + * + * The verifier is templated on `Curve`: + * - `curve::Grumpkin` — native verification (bool return, early exit) + * - `stdlib::grumpkin` — in-circuit verification (constraints, no early exit) + * + * The prover is always native. + * + * See OPTIMIZATIONS.md for circuit cost analysis. + */ + +#include "ecfft_domain.hpp" + +#include "barretenberg/common/assert.hpp" +#include "barretenberg/common/thread.hpp" +#include "barretenberg/crypto/merkle_tree/hash.hpp" +#include "barretenberg/crypto/merkle_tree/memory_tree.hpp" +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include "barretenberg/stdlib/hash/poseidon2/poseidon2.hpp" +#include "barretenberg/stdlib/primitives/curves/grumpkin.hpp" +#include "barretenberg/stdlib/proof/proof.hpp" +#include "barretenberg/transcript/transcript.hpp" + +#include +#include + +namespace bb::basefold { + +// Native Grumpkin types (used by the prover and native Merkle helpers). +using NativeCommitment = grumpkin::g1::affine_element; +using NativeGroupElement = grumpkin::g1::element; +using MerkleTree = crypto::merkle_tree::MemoryTree; + +// --------------------------------------------------------------------------- +// Merkle tree helpers (always native — Merkle trees are prover-side) +// --------------------------------------------------------------------------- + +inline fr hash_group_element(const NativeCommitment& point) +{ + return crypto::Poseidon2::hash({ point.x, point.y }); +} + +inline std::pair build_merkle_tree(const std::vector& elements) +{ + size_t n = elements.size(); + BB_ASSERT((n & (n - 1)) == 0); + size_t depth = static_cast(numeric::get_msb(static_cast(n))); + + MerkleTree tree(depth); + for (size_t i = 0; i < n; i++) { + tree.update_element(i, hash_group_element(elements[i])); + } + return { std::move(tree), tree.root() }; +} + +inline bool verify_merkle_opening(const fr& root, + size_t index, + const NativeCommitment& element, + const crypto::merkle_tree::fr_sibling_path& path) +{ + fr current = hash_group_element(element); + for (size_t i = 0; i < path.size(); i++) { + if (index % 2 == 0) { + current = crypto::merkle_tree::Poseidon2HashPolicy::hash_pair(current, path[i]); + } else { + current = crypto::merkle_tree::Poseidon2HashPolicy::hash_pair(path[i], current); + } + index >>= 1; + } + return current == root; +} + +// --------------------------------------------------------------------------- +// Prover-side fold (always native) +// --------------------------------------------------------------------------- + +inline std::vector fold_group_oracle(const std::vector& oracle, + const EcfftDomain& domain, + size_t round_idx, + size_t degree_bound, + const Fq& z) +{ + size_t m = oracle.size(); + size_t half = m / 2; + std::vector folded(half); + + parallel_for(half, [&](size_t j) { + NativeGroupElement result = domain.fold_pair( + round_idx, degree_bound, j, NativeGroupElement(oracle[j]), NativeGroupElement(oracle[j + half]), z); + folded[j] = result.normalize(); + }); + + return folded; +} + +// --------------------------------------------------------------------------- +// Native prover +// --------------------------------------------------------------------------- + +inline void prove(const std::vector& g0, + const EcfftDomain& domain, + size_t degree_bound, + size_t num_queries, + const std::shared_ptr& transcript) +{ + size_t num_rounds = domain.num_rounds; + + std::vector> oracles; + std::vector trees; + std::vector roots; + + oracles.push_back(g0); + size_t d = degree_bound; + + // Phase 1: Commit & fold + for (size_t round = 0; round < num_rounds; round++) { + auto [tree, root] = build_merkle_tree(oracles.back()); + transcript->send_to_verifier("basefold_root_" + std::to_string(round), root); + trees.push_back(std::move(tree)); + roots.push_back(root); + + Fq z = transcript->template get_challenge("basefold_challenge_" + std::to_string(round)); + auto folded = fold_group_oracle(oracles.back(), domain, round, d, z); + d /= 2; + oracles.push_back(std::move(folded)); + } + + BB_ASSERT(oracles.back().size() == 1); + transcript->send_to_verifier("basefold_final", oracles.back()[0]); + + // Phase 2: Query openings + fr query_seed = transcript->template get_challenge("basefold_query_seed"); + + for (size_t q = 0; q < num_queries; q++) { + fr idx_field = crypto::Poseidon2::hash( + { query_seed, fr(static_cast(q)) }); + size_t idx = static_cast(idx_field.reduce_once().data[0] % domain.levels[0].size()); + + size_t current_idx = idx; + for (size_t round = 0; round < num_rounds; round++) { + size_t m = domain.levels[round].size(); + size_t half = m / 2; + size_t j = current_idx % half; + + auto path_0 = trees[round].get_sibling_path(j); + auto path_1 = trees[round].get_sibling_path(j + half); + + std::string prefix = "basefold_r" + std::to_string(round) + "_q" + std::to_string(q); + transcript->send_to_verifier(prefix + "_e0", oracles[round][j]); + for (size_t pi = 0; pi < path_0.size(); pi++) { + transcript->send_to_verifier(prefix + "_p0_" + std::to_string(pi), path_0[pi]); + } + transcript->send_to_verifier(prefix + "_e1", oracles[round][j + half]); + for (size_t pi = 0; pi < path_1.size(); pi++) { + transcript->send_to_verifier(prefix + "_p1_" + std::to_string(pi), path_1[pi]); + } + transcript->send_to_verifier(prefix + "_fold", oracles[round + 1][j]); + + current_idx = j; + } + } +} + +// --------------------------------------------------------------------------- +// Native verifier +// --------------------------------------------------------------------------- + +/** + * @brief Native BaseFold verifier. Returns false on first failure. + */ +inline bool verify(const EcfftDomain& domain, + size_t degree_bound, + size_t num_queries, + const std::shared_ptr& transcript) +{ + size_t num_rounds = domain.num_rounds; + + std::vector roots(num_rounds); + std::vector challenges(num_rounds); + + for (size_t round = 0; round < num_rounds; round++) { + roots[round] = transcript->template receive_from_prover("basefold_root_" + std::to_string(round)); + challenges[round] = transcript->template get_challenge("basefold_challenge_" + std::to_string(round)); + } + + // basefold_final is absorbed into Fiat-Shamir before the query seed is derived. + // The verifier MUST check it matches the actual last-round fold result to prevent + // the prover from manipulating query indices via a fake basefold_final. + auto g_final = transcript->template receive_from_prover("basefold_final"); + + fr query_seed = transcript->template get_challenge("basefold_query_seed"); + + for (size_t q = 0; q < num_queries; q++) { + fr idx_field = crypto::Poseidon2::hash( + { query_seed, fr(static_cast(q)) }); + size_t idx = static_cast(idx_field.reduce_once().data[0] % domain.levels[0].size()); + + size_t current_idx = idx; + size_t current_d = degree_bound; + + for (size_t round = 0; round < num_rounds; round++) { + size_t m = domain.levels[round].size(); + size_t half = m / 2; + size_t j = current_idx % half; + + size_t tree_depth = static_cast(numeric::get_msb(static_cast(m))); + std::string prefix = "basefold_r" + std::to_string(round) + "_q" + std::to_string(q); + + auto elem_0 = transcript->template receive_from_prover(prefix + "_e0"); + crypto::merkle_tree::fr_sibling_path path_0(tree_depth); + for (size_t pi = 0; pi < tree_depth; pi++) { + path_0[pi] = transcript->template receive_from_prover(prefix + "_p0_" + std::to_string(pi)); + } + auto elem_1 = transcript->template receive_from_prover(prefix + "_e1"); + crypto::merkle_tree::fr_sibling_path path_1(tree_depth); + for (size_t pi = 0; pi < tree_depth; pi++) { + path_1[pi] = transcript->template receive_from_prover(prefix + "_p1_" + std::to_string(pi)); + } + auto claimed_fold = transcript->template receive_from_prover(prefix + "_fold"); + + if (!verify_merkle_opening(roots[round], j, elem_0, path_0)) { + return false; + } + if (!verify_merkle_opening(roots[round], j + half, elem_1, path_1)) { + return false; + } + + NativeGroupElement expected_fold = domain.fold_pair( + round, current_d, j, NativeGroupElement(elem_0), NativeGroupElement(elem_1), challenges[round]); + + if (NativeCommitment(expected_fold.normalize()) != claimed_fold) { + return false; + } + + // At the last round, verify the fold result matches basefold_final. + // This binds the committed final value to the actual fold computation, + // preventing the prover from choosing a fake basefold_final to manipulate + // the Fiat-Shamir-derived query indices. + if (round == num_rounds - 1) { + if (claimed_fold != g_final) { + return false; + } + } + + current_idx = j; + current_d /= 2; + } + } + + return true; +} + +// --------------------------------------------------------------------------- +// Recursive (in-circuit) verifier +// --------------------------------------------------------------------------- + +/** + * @brief BaseFold recursive verifier — builds constraints in a UltraCircuitBuilder. + * + * Reads the proof from a stdlib transcript and adds constraints for: + * 1. Merkle path verification via stdlib::poseidon2 + * 2. Fold consistency via cycle_group scalar multiplications + * + * The fold check per round uses 4 group operations (3 with constant scalars, + * 1 with witness scalar), matching the native fold_pair formula. No cross-query + * batching is done — each query × round check is independent. + * + * @tparam Builder The circuit builder type (e.g. UltraCircuitBuilder). + */ +template class RecursiveBaseFoldVerifier { + public: + using Curve = bb::stdlib::grumpkin; + using field_ct = bb::stdlib::field_t; + using witness_ct = bb::stdlib::witness_t; + using bool_ct = bb::stdlib::bool_t; + using group_ct = bb::stdlib::cycle_group; + using bigfield_ct = typename Curve::ScalarField; // bigfield + using Poseidon2 = bb::stdlib::poseidon2; + using StdlibTranscript = bb::StdlibTranscript; + using StdlibProof = bb::stdlib::Proof; + + /** + * @brief Verify a BaseFold proof in-circuit. + * + * Uses a "native hint" approach: runs the native transcript to extract all + * values and challenges, then brings them into the circuit as witnesses. + * This avoids origin tag conflicts from the stdlib transcript codec while + * producing an identical circuit. + * + * @param builder The circuit builder. + * @param domain The ECFFT domain (native, known at compile/setup time). + * @param degree_bound Initial degree bound. + * @param num_queries Number of queries. + * @param native_proof The native proof data (vector of fr). + */ + static void verify(Builder& builder, + const EcfftDomain& domain, + size_t degree_bound, + size_t num_queries, + const std::vector& native_proof) + { + size_t num_rounds = domain.num_rounds; + + // === Step 1: Run native transcript to extract all values and challenges === + auto native_transcript = std::make_shared(native_proof); + + std::vector roots_native(num_rounds); + std::vector challenges_native(num_rounds); + + for (size_t round = 0; round < num_rounds; round++) { + roots_native[round] = + native_transcript->template receive_from_prover("basefold_root_" + std::to_string(round)); + challenges_native[round] = + native_transcript->template get_challenge("basefold_challenge_" + std::to_string(round)); + } + + auto g_final_native = native_transcript->template receive_from_prover("basefold_final"); + + fr query_seed_native = native_transcript->template get_challenge("basefold_query_seed"); + + // === Step 2: Bring values into circuit as witnesses === + std::vector roots(num_rounds); + std::vector challenges_ct(num_rounds); + + for (size_t round = 0; round < num_rounds; round++) { + roots[round] = witness_ct(&builder, roots_native[round]); + challenges_ct[round] = bigfield_ct::from_witness(&builder, challenges_native[round]); + } + + auto g_final = group_ct::from_witness(&builder, g_final_native); + + // === Step 3: Query phase === + for (size_t q = 0; q < num_queries; q++) { + // Derive query index natively (deterministic) + fr idx_field = crypto::Poseidon2::hash( + { query_seed_native, fr(static_cast(q)) }); + size_t idx = static_cast(idx_field.reduce_once().data[0] % domain.levels[0].size()); + + size_t current_idx = idx; + size_t current_d = degree_bound; + + for (size_t round = 0; round < num_rounds; round++) { + size_t m = domain.levels[round].size(); + size_t half = m / 2; + size_t j = current_idx % half; + + size_t tree_depth = static_cast(numeric::get_msb(static_cast(m))); + std::string prefix = "basefold_r" + std::to_string(round) + "_q" + std::to_string(q); + + // Read native values from transcript, bring into circuit as witnesses + auto elem_0_native = native_transcript->template receive_from_prover(prefix + "_e0"); + auto G0 = group_ct::from_witness(&builder, elem_0_native); + + std::vector path_0(tree_depth); + for (size_t pi = 0; pi < tree_depth; pi++) { + auto p = native_transcript->template receive_from_prover(prefix + "_p0_" + std::to_string(pi)); + path_0[pi] = witness_ct(&builder, p); + } + + auto elem_1_native = native_transcript->template receive_from_prover(prefix + "_e1"); + auto G1 = group_ct::from_witness(&builder, elem_1_native); + + std::vector path_1(tree_depth); + for (size_t pi = 0; pi < tree_depth; pi++) { + auto p = native_transcript->template receive_from_prover(prefix + "_p1_" + std::to_string(pi)); + path_1[pi] = witness_ct(&builder, p); + } + + auto fold_native = native_transcript->template receive_from_prover(prefix + "_fold"); + auto claimed_fold = group_ct::from_witness(&builder, fold_native); + + // Check 1: Merkle path verification (stdlib Poseidon2) + verify_merkle_path_circuit(builder, roots[round], j, G0, path_0); + verify_merkle_path_circuit(builder, roots[round], j + half, G1, path_1); + + // Check 2: Fold consistency (cycle_group scalar muls) + auto expected_fold = fold_pair_circuit(domain, round, current_d, j, G0, G1, challenges_ct[round]); + + expected_fold.assert_equal(claimed_fold); + + // Check 3: At the last round, the fold result must match basefold_final + if (round == num_rounds - 1) { + claimed_fold.assert_equal(g_final); + } + + current_idx = j; + current_d /= 2; + } + } + } + + private: + /** + * @brief Verify a Merkle path in-circuit using stdlib Poseidon2. + * + * Hashes the leaf, then walks up the path using conditional_assign to + * select hash argument order based on the index bit. This produces a + * FIXED circuit regardless of the query index value — the same gates + * are created for every index, with conditional_assign selecting which + * value goes left vs right. + * + * Cost: 2 Poseidon2 hashes per level (both orderings) + 1 conditional_assign. + * This is ~2× the Merkle cost of the native verifier but ensures a + * witness-independent circuit topology. + */ + static void verify_merkle_path_circuit(Builder& builder, + const field_ct& root, + size_t index, + const group_ct& element, + const std::vector& path) + { + // Leaf hash: Poseidon2(x, y) + field_ct current = Poseidon2::hash({ element.x(), element.y() }); + + // Walk up the Merkle tree. At each level, compute BOTH hash orderings + // and select based on the index bit. This makes the circuit topology + // independent of the query index. + for (size_t i = 0; i < path.size(); i++) { + field_ct hash_left_right = Poseidon2::hash({ current, path[i] }); + field_ct hash_right_left = Poseidon2::hash({ path[i], current }); + + // index_bit = 1 means current is on the right (odd index) + bool_ct index_bit = bool_ct(witness_ct(&builder, (index % 2 != 0))); + current = field_ct::conditional_assign(index_bit, hash_right_left, hash_left_right); + + index >>= 1; + } + + current.assert_equal(root); + } + + /** + * @brief Compute the fold formula in-circuit using cycle_group operations. + * + * Implements: + * a = G0 · s0^{-e} (witness point × constant scalar) + * b = G1 · s1^{-e} (witness point × constant scalar) + * slope = (b - a) · diff_inv (witness point × constant scalar) + * result = a + slope · (z - s0) (witness point × witness scalar) + * + * When e == 0: + * slope = (G1 - G0) · diff_inv (witness point × constant scalar) + * result = G0 + slope · (z - s0) (witness point × witness scalar) + * + * The constant scalars are bigfield constants (no circuit cost). + * The witness scalar (z - s0) involves one bigfield subtraction. + */ + static group_ct fold_pair_circuit(const EcfftDomain& domain, + size_t round_idx, + size_t degree_bound, + size_t j, + const group_ct& G0, + const group_ct& G1, + const bigfield_ct& z_ct) + { + Builder* ctx = z_ct.get_context(); + const auto& level = domain.levels[round_idx]; + size_t half = level.size() / 2; + size_t e = degree_bound / 2 - 1; + + Fq s0 = level.domain[j]; + Fq s1 = level.domain[j + half]; + Fq diff_inv = level.pair_diff_inv[j]; + + // Constant bigfield values — constructed with builder context so their + // internal field_t limbs have proper context (needed for origin tag checks). + bigfield_ct diff_inv_ct(ctx, uint256_t(diff_inv)); + bigfield_ct s0_ct(ctx, uint256_t(s0)); + + // (z - s0) is the only witness-dependent scalar + bigfield_ct z_minus_s0 = z_ct - s0_ct; + + if (e == 0) { + auto slope = (G1 - G0) * diff_inv_ct; + return G0 + slope * z_minus_s0; + } + + Fq s0_e_inv = s0.pow(e).invert(); + Fq s1_e_inv = s1.pow(e).invert(); + + bigfield_ct s0_e_inv_ct(ctx, uint256_t(s0_e_inv)); + bigfield_ct s1_e_inv_ct(ctx, uint256_t(s1_e_inv)); + + auto a = G0 * s0_e_inv_ct; + auto b = G1 * s1_e_inv_ct; + auto slope = (b - a) * diff_inv_ct; + return a + slope * z_minus_s0; + } +}; + +// --------------------------------------------------------------------------- +// SRS encoding (one-time precomputation, always native) +// --------------------------------------------------------------------------- + +inline std::vector compute_srs_encoding(const std::vector& srs_generators, + const EcfftDomain& domain) +{ + const auto& L0 = domain.levels[0].domain; + size_t domain_size = L0.size(); + size_t n = srs_generators.size(); + + std::vector g0(domain_size); + parallel_for(domain_size, [&](size_t j) { + Fq x = L0[j]; + NativeGroupElement acc = NativeGroupElement::infinity(); + Fq x_pow = Fq::one(); + for (size_t i = 0; i < n; i++) { + acc = acc + NativeGroupElement(srs_generators[i]) * x_pow; + x_pow *= x; + } + g0[j] = acc.normalize(); + }); + return g0; +} + +} // namespace bb::basefold diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold.test.cpp b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold.test.cpp new file mode 100644 index 000000000000..89ea4ab1fe3f --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold.test.cpp @@ -0,0 +1,217 @@ +#include "basefold.hpp" +#include "ecfft_domain.hpp" +#include "ecfft_domain_data_2_8.hpp" + +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include "barretenberg/numeric/random/engine.hpp" + +#include + +namespace bb::basefold { + +namespace { + +/** + * @brief Build the test domain from the generated hex data. + */ +EcfftDomain build_test_domain() +{ + using namespace domain_data; + + std::vector> layer_hex; + std::vector> diff_inv_hex; + + // Layer 0-8 and their diff_inv arrays + layer_hex.push_back({ LAYER_0.data(), LAYER_0.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_0.data(), PAIR_DIFF_INV_0.size() }); + + layer_hex.push_back({ LAYER_1.data(), LAYER_1.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_1.data(), PAIR_DIFF_INV_1.size() }); + + layer_hex.push_back({ LAYER_2.data(), LAYER_2.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_2.data(), PAIR_DIFF_INV_2.size() }); + + layer_hex.push_back({ LAYER_3.data(), LAYER_3.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_3.data(), PAIR_DIFF_INV_3.size() }); + + layer_hex.push_back({ LAYER_4.data(), LAYER_4.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_4.data(), PAIR_DIFF_INV_4.size() }); + + layer_hex.push_back({ LAYER_5.data(), LAYER_5.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_5.data(), PAIR_DIFF_INV_5.size() }); + + layer_hex.push_back({ LAYER_6.data(), LAYER_6.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_6.data(), PAIR_DIFF_INV_6.size() }); + + layer_hex.push_back({ LAYER_7.data(), LAYER_7.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_7.data(), PAIR_DIFF_INV_7.size() }); + + // Final layer (size 1, no diff_inv) + layer_hex.push_back({ LAYER_8.data(), LAYER_8.size() }); + + return EcfftDomain::from_hex_arrays(LOG_N, layer_hex, diff_inv_hex); +} + +} // anonymous namespace + +class BaseFoldTest : public ::testing::Test { + protected: + void SetUp() override { domain = build_test_domain(); } + + EcfftDomain domain; +}; + +TEST_F(BaseFoldTest, DomainLoadsCorrectly) +{ + EXPECT_EQ(domain.log_n, 8); + EXPECT_EQ(domain.num_rounds, 8); + EXPECT_EQ(domain.levels.size(), 9); + EXPECT_EQ(domain.levels[0].size(), 256); + EXPECT_EQ(domain.levels[1].size(), 128); + EXPECT_EQ(domain.levels[8].size(), 1); + EXPECT_EQ(domain.levels[0].num_pairs(), 128); +} + +TEST_F(BaseFoldTest, FoldPairScalar) +{ + // Test fold_pair with scalar field elements. + // Use a degree-1 polynomial f(x) = 1 + 2x evaluated on L_0. + // After folding with degree_bound = 256 and challenge z, the result + // should be a degree-0 polynomial (constant) on L_1. + + size_t n = domain.levels[0].size(); + Fq z(42); + + // Evaluate f(x) = 1 + 2x on L_0 + std::vector evals(n); + for (size_t j = 0; j < n; j++) { + evals[j] = Fq(1) + Fq(2) * domain.levels[0].domain[j]; + } + + // Fold all pairs + size_t half = n / 2; + std::vector folded(half); + for (size_t j = 0; j < half; j++) { + folded[j] = domain.fold_pair(0, n, j, evals[j], evals[j + half], z); + } + + // Check: verify_query should match + for (size_t j = 0; j < std::min(half, size_t(8)); j++) { + Fq expected = domain.fold_pair(0, n, j, evals[j], evals[j + half], z); + EXPECT_EQ(folded[j], expected); + } +} + +TEST_F(BaseFoldTest, FoldGroupElement) +{ + // Test fold_pair with group elements. + // Create random group elements and verify fold is consistent. + auto& engine = numeric::get_debug_randomness(); + + size_t n = domain.levels[0].size(); + Fq z = Fq::random_element(&engine); + + // Random group elements + std::vector g_oracle(n); + for (size_t i = 0; i < n; i++) { + g_oracle[i] = grumpkin::g1::element::random_element(&engine).normalize(); + } + + // Fold using the prover function + auto folded = fold_group_oracle(g_oracle, domain, 0, n, z); + EXPECT_EQ(folded.size(), n / 2); + + // Verify fold_pair gives the same result for a few indices + for (size_t j = 0; j < 4; j++) { + NativeGroupElement expected = domain.fold_pair( + 0, n, j, NativeGroupElement(g_oracle[j]), NativeGroupElement(g_oracle[j + n / 2]), z); + EXPECT_EQ(NativeCommitment(expected.normalize()), folded[j]); + } +} + +TEST_F(BaseFoldTest, MerkleTreeRoundTrip) +{ + auto& engine = numeric::get_debug_randomness(); + + // Build a small Merkle tree over random group elements + size_t n = 16; + std::vector elements(n); + for (size_t i = 0; i < n; i++) { + elements[i] = grumpkin::g1::element::random_element(&engine).normalize(); + } + + auto [tree, root] = build_merkle_tree(elements); + + // Verify openings at a few indices + for (size_t i = 0; i < n; i++) { + auto path = tree.get_sibling_path(i); + EXPECT_TRUE(verify_merkle_opening(root, i, elements[i], path)); + } + + // Tampering should fail + auto bad_element = grumpkin::g1::element::random_element(&engine).normalize(); + auto path = tree.get_sibling_path(0); + EXPECT_FALSE(verify_merkle_opening(root, 0, bad_element, path)); +} + +TEST_F(BaseFoldTest, ProverVerifierRoundTrip) +{ + // Full prover-verifier round trip with random group elements. + auto& engine = numeric::get_debug_randomness(); + + size_t n = domain.levels[0].size(); // 256 + size_t degree_bound = n; + size_t num_queries = 4; // small for testing + + // Random group elements as the "SRS encoding" + std::vector g0(n); + for (size_t i = 0; i < n; i++) { + g0[i] = grumpkin::g1::element::random_element(&engine).normalize(); + } + + // Prove + auto prover_transcript = std::make_shared(); + prove(g0, domain, degree_bound, num_queries, prover_transcript); + + // Verify + auto verifier_transcript = std::make_shared(prover_transcript->export_proof()); + bool result = verify(domain, degree_bound, num_queries, verifier_transcript); + EXPECT_TRUE(result); +} + +TEST_F(BaseFoldTest, SoundnessRejectsTamperedOracle) +{ + auto& engine = numeric::get_debug_randomness(); + + size_t n = domain.levels[0].size(); + size_t degree_bound = n; + size_t num_queries = 4; + + std::vector g0(n); + for (size_t i = 0; i < n; i++) { + g0[i] = grumpkin::g1::element::random_element(&engine).normalize(); + } + + // Prove with correct oracle + auto prover_transcript = std::make_shared(); + prove(g0, domain, degree_bound, num_queries, prover_transcript); + + // Tamper with a field element in the proof + auto proof_data = prover_transcript->export_proof(); + if (!proof_data.empty()) { + proof_data[proof_data.size() / 2] += fr(1); + } + + // Verification should fail (with high probability) + auto verifier_transcript = std::make_shared(proof_data); + // Note: tampered transcript may throw or return false + bool result = false; + try { + result = verify(domain, degree_bound, num_queries, verifier_transcript); + } catch (...) { + result = false; + } + EXPECT_FALSE(result); +} + +} // namespace bb::basefold diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold_circuit_cost.test.cpp b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold_circuit_cost.test.cpp new file mode 100644 index 000000000000..563c255ff37e --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/basefold_circuit_cost.test.cpp @@ -0,0 +1,483 @@ +/** + * @brief Gate count estimation for the BaseFold recursive verifier circuit. + * + * Simulates the verifier's in-circuit operations for a 2^15 MSM with + * FRI blowup factor 8 (domain size 2^18, 18 fold rounds, 43 queries). + * + * Reports gate counts for: + * - Single fold consistency check (4 Grumpkin scalar muls) + * - Single Merkle path verification (Poseidon2 hashes) + * - One complete query (18 rounds of fold + Merkle) + * - Full verifier (43 queries) + * + * Also computes native proof size. + */ +#include "basefold.hpp" +#include "ecfft_domain.hpp" +#include "ecfft_domain_data_2_8.hpp" + +#include "barretenberg/circuit_checker/circuit_checker.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/numeric/random/engine.hpp" +#include "barretenberg/stdlib/hash/poseidon2/poseidon2.hpp" +#include "barretenberg/stdlib/primitives/curves/grumpkin.hpp" +#include "barretenberg/stdlib/primitives/group/cycle_group.hpp" +#include "barretenberg/stdlib_circuit_builders/ultra_circuit_builder.hpp" + +#include +#include +#include + +namespace { + +using Builder = bb::UltraCircuitBuilder; +using field_ct = bb::stdlib::field_t; +using witness_ct = bb::stdlib::witness_t; +using cycle_group_ct = bb::stdlib::cycle_group; +using cycle_scalar_ct = bb::stdlib::cycle_scalar; +using Poseidon2 = bb::stdlib::poseidon2; +using Fq_native = bb::fq; // Grumpkin scalar field = BN254 base field (domain elements live here) +using Fr_native = bb::fr; // BN254 scalar field = Grumpkin base field (circuit native field) +using Grumpkin = bb::grumpkin::g1; + +// Parameters for 2^15 MSM with blowup 8 +constexpr size_t LOG_MSM_SIZE = 15; +constexpr size_t BLOWUP_BITS = 3; +constexpr size_t LOG_DOMAIN_SIZE = LOG_MSM_SIZE + BLOWUP_BITS; // 18 +constexpr size_t NUM_FOLD_ROUNDS = LOG_DOMAIN_SIZE; // 18 +constexpr size_t NUM_QUERIES = 43; // ~128 bits security + +/** + * @brief Measure gates for a single fold consistency check. + * + * The fold formula (when e > 0) does 4 operations on group elements: + * a = G_0 · s0^{-e} (witness point × constant scalar) + * b = G_1 · s1^{-e} (witness point × constant scalar) + * slope = (b - a) · diff_inv (witness point × constant scalar) + * result = a + slope · (z-s0) (witness point × witness scalar) + * + * Three of these use constant scalars (precomputed from the domain), which + * cycle_group handles more cheaply than witness scalars. Only the final + * multiplication by (z - s0) involves a witness scalar. + * + * When e == 0 (last round, degree_bound == 2), no normalization is needed + * and there are only 2 operations. + * + * Note: the α,β reformulation (fold = G_0·α + G_1·β) was benchmarked and is + * SLOWER (~10,200 gates) because α,β are witness bigfield values requiring + * expensive non-native field arithmetic (~5,100 gates). The 4-mul form wins + * because constant-scalar muls are cheap in cycle_group. + */ +size_t measure_fold_check_gates(bool e_is_zero) +{ + auto& engine = bb::numeric::get_debug_randomness(); + Builder builder; + + // Two witness Grumpkin points (the opened pair) + auto p0_native = Grumpkin::element::random_element(&engine).normalize(); + auto p1_native = Grumpkin::element::random_element(&engine).normalize(); + auto G0 = cycle_group_ct::from_witness(&builder, p0_native); + auto G1 = cycle_group_ct::from_witness(&builder, p1_native); + + // Constant scalars (precomputed from domain) + auto s0_e_inv_native = Fq_native::random_element(&engine); + auto s1_e_inv_native = Fq_native::random_element(&engine); + auto diff_inv_native = Fq_native::random_element(&engine); + auto s0_native = Fq_native::random_element(&engine); + + // Witness challenge z (from transcript) + auto z_native = Fq_native::random_element(&engine); + + using BigScalarField = bb::stdlib::bigfield; + + if (e_is_zero) { + // slope = (G1 - G0) · diff_inv [witness point × constant scalar] + auto diff = G1 - G0; + auto diff_inv_scalar = BigScalarField(diff_inv_native); // constant + auto slope = diff * diff_inv_scalar; + + // result = G0 + slope · (z - s0) [witness point × witness scalar] + auto z_scalar = BigScalarField::from_witness(&builder, z_native); + auto s0_scalar = BigScalarField(s0_native); // constant + auto z_minus_s0 = z_scalar - s0_scalar; + auto result = G0 + slope * z_minus_s0; + + static_cast(result.get_value()); + } else { + // a = G0 · s0^{-e} [witness point × constant scalar] + auto s0_e_inv_scalar = BigScalarField(s0_e_inv_native); // constant + auto a = G0 * s0_e_inv_scalar; + + // b = G1 · s1^{-e} [witness point × constant scalar] + auto s1_e_inv_scalar = BigScalarField(s1_e_inv_native); // constant + auto b = G1 * s1_e_inv_scalar; + + // slope = (b - a) · diff_inv [witness point × constant scalar] + auto diff = b - a; + auto diff_inv_scalar = BigScalarField(diff_inv_native); // constant + auto slope = diff * diff_inv_scalar; + + // result = a + slope · (z - s0) [witness point × witness scalar] + auto z_scalar = BigScalarField::from_witness(&builder, z_native); + auto s0_scalar = BigScalarField(s0_native); // constant + auto z_minus_s0 = z_scalar - s0_scalar; + auto result = a + slope * z_minus_s0; + + static_cast(result.get_value()); + } + + builder.finalize_circuit(/*ensure_nonzero=*/false); + return builder.get_num_finalized_gates(); +} + +/** + * @brief Measure gates for Merkle path verification of depth `depth`. + * + * Each level: Poseidon2 hash of 2 field elements. + * Leaf hash: Poseidon2(x, y) for a Grumpkin point. + * We hash x,y as circuit witnesses (the full point). + * + * For the "x-only" variant, leaf hash is Poseidon2(x) and we do + * an on-curve check: y^2 == x^3 + b. + */ +size_t measure_merkle_path_gates(size_t depth, bool x_only_leaves) +{ + auto& engine = bb::numeric::get_debug_randomness(); + Builder builder; + + // Leaf element (witness Grumpkin point) + auto pt_native = Grumpkin::element::random_element(&engine).normalize(); + field_ct x_ct = witness_ct(&builder, pt_native.x); + field_ct y_ct = witness_ct(&builder, pt_native.y); + + // Leaf hash + field_ct current; + if (x_only_leaves) { + // Hash only x, then constrain y^2 = x^3 + b + current = Poseidon2::hash({ x_ct }); + auto x3 = x_ct * x_ct * x_ct; + auto b_ct = field_ct(&builder, bb::grumpkin::g1::curve_b); + auto y_sq = y_ct * y_ct; + auto rhs = x3 + b_ct; + y_sq.assert_equal(rhs); + } else { + current = Poseidon2::hash({ x_ct, y_ct }); + } + + // Path hashes + for (size_t i = 0; i < depth; i++) { + field_ct sibling = witness_ct(&builder, Fr_native::random_element(&engine)); + // Alternate left/right (doesn't matter for gate count) + if (i % 2 == 0) { + current = Poseidon2::hash({ current, sibling }); + } else { + current = Poseidon2::hash({ sibling, current }); + } + } + + // Constrain root + field_ct expected_root = witness_ct(&builder, Fr_native::random_element(&engine)); + current.assert_equal(expected_root); + + builder.finalize_circuit(/*ensure_nonzero=*/false); + return builder.get_num_finalized_gates(); +} + +/** + * @brief Compute native proof size for the BaseFold protocol. + * + * Proof elements (all in BN254 Fr): + * - num_rounds Merkle roots: num_rounds * 1 Fr + * - 1 final group element: 2 Fr (x, y) + * - Per query, per round: + * - 2 group element openings: 2 * 2 Fr = 4 Fr + * - 2 Merkle paths of depth log2(oracle_size): 2 * depth Fr + * - 1 fold result: 2 Fr + * Total per query per round: 6 + 2*depth Fr + */ +void print_proof_size() +{ + size_t num_rounds = NUM_FOLD_ROUNDS; + size_t num_queries = NUM_QUERIES; + + // Fixed part + size_t fixed_fr = num_rounds + 2; // roots + final element + + // Per-query part + size_t per_query_fr = 0; + for (size_t round = 0; round < num_rounds; round++) { + size_t log_oracle = LOG_DOMAIN_SIZE - round; + size_t depth = log_oracle; + // 2 openings (2 Fr each) + 2 paths (depth Fr each) + 1 fold result (2 Fr) + per_query_fr += 4 + 2 * depth + 2; + } + + size_t total_fr = fixed_fr + num_queries * per_query_fr; + size_t total_bytes = total_fr * 32; // each Fr is 32 bytes + + info("=== Native Proof Size ==="); + info(" Rounds: ", num_rounds); + info(" Queries: ", num_queries); + info(" Fixed overhead: ", fixed_fr, " Fr elements"); + info(" Per query: ", per_query_fr, " Fr elements"); + info(" Total: ", total_fr, " Fr elements = ", total_bytes, " bytes = ", total_bytes / 1024, " KiB"); +} + +class BaseFoldCircuitCostTest : public ::testing::Test {}; + +TEST_F(BaseFoldCircuitCostTest, FoldCheckGates) +{ + size_t gates_e_nonzero = measure_fold_check_gates(/*e_is_zero=*/false); + size_t gates_e_zero = measure_fold_check_gates(/*e_is_zero=*/true); + + info("=== Fold Consistency Check Gates ==="); + info(" e > 0 (4 ops: 3 const-scalar + 1 witness-scalar): ", gates_e_nonzero, " gates"); + info(" e == 0 (2 ops: 1 const-scalar + 1 witness-scalar): ", gates_e_zero, " gates"); +} + +TEST_F(BaseFoldCircuitCostTest, MerklePathGates) +{ + info("=== Merkle Path Verification Gates ==="); + info(" (hash x,y leaves):"); + for (size_t depth : { size_t(1), size_t(5), size_t(10), size_t(18) }) { + size_t gates = measure_merkle_path_gates(depth, /*x_only_leaves=*/false); + info(" depth ", depth, ": ", gates, " gates"); + } + info(" (hash x-only leaves, with on-curve check):"); + for (size_t depth : { size_t(1), size_t(5), size_t(10), size_t(18) }) { + size_t gates = measure_merkle_path_gates(depth, /*x_only_leaves=*/true); + info(" depth ", depth, ": ", gates, " gates"); + } +} + +TEST_F(BaseFoldCircuitCostTest, FullVerifierEstimate) +{ + // Measure building blocks + size_t fold_e_nonzero = measure_fold_check_gates(false); + size_t fold_e_zero = measure_fold_check_gates(true); + + // Merkle depth per round: round r has oracle size 2^{18-r}, depth = 18-r. + // Two paths per round per query. + size_t merkle_total_per_query = 0; + for (size_t round = 0; round < NUM_FOLD_ROUNDS; round++) { + size_t depth = LOG_DOMAIN_SIZE - round; + size_t gates = measure_merkle_path_gates(depth, /*x_only_leaves=*/false); + merkle_total_per_query += 2 * gates; // 2 paths per round + } + + // Fold gates per query: 17 rounds with e>0, 1 round with e==0 + size_t fold_per_query = ((NUM_FOLD_ROUNDS - 1) * fold_e_nonzero) + fold_e_zero; + + size_t per_query_total = fold_per_query + merkle_total_per_query; + + info("=== Full Verifier Gate Estimate (", LOG_MSM_SIZE, "-bit MSM, blowup ", (1 << BLOWUP_BITS), ") ==="); + info(" Fold rounds: ", NUM_FOLD_ROUNDS); + info(" Queries: ", NUM_QUERIES); + info(""); + info(" Fold gates per query: ", fold_per_query); + info(" (", NUM_FOLD_ROUNDS - 1, " rounds e>0 @ ", fold_e_nonzero, " + 1 round e==0 @ ", fold_e_zero, ")"); + info(" Merkle gates per query (hash x,y): ", merkle_total_per_query); + info(""); + info(" Per query total: ", per_query_total); + info(" Full verifier (", NUM_QUERIES, " queries): ", NUM_QUERIES * per_query_total, " gates"); + info(" Log2: ", std::log2(static_cast(NUM_QUERIES * per_query_total))); + + // Also print proof size + info(""); + print_proof_size(); +} + +/** + * @brief Build the ACTUAL recursive verifier circuit and count gates. + * + * Uses the log_n=8 test domain (domain size 256, 8 rounds) to construct a real + * native proof, then verifies it inside a UltraCircuitBuilder using + * RecursiveBaseFoldVerifier. Reports the concrete gate count. + * + * The log_n=8 domain is small enough to run quickly but exercises the full + * prover-verifier pipeline. Gate counts scale linearly with num_queries and + * per-round costs are representative of the log_n=18 production case. + */ +TEST_F(BaseFoldCircuitCostTest, ConcreteRecursiveVerifier) +{ + using namespace bb::basefold; + + auto& engine = bb::numeric::get_debug_randomness(); + + // Build the log_n=8 test domain + auto build_domain = []() { + using namespace bb::basefold::domain_data; + std::vector> layer_hex; + std::vector> diff_inv_hex; + layer_hex.push_back({ LAYER_0.data(), LAYER_0.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_0.data(), PAIR_DIFF_INV_0.size() }); + layer_hex.push_back({ LAYER_1.data(), LAYER_1.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_1.data(), PAIR_DIFF_INV_1.size() }); + layer_hex.push_back({ LAYER_2.data(), LAYER_2.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_2.data(), PAIR_DIFF_INV_2.size() }); + layer_hex.push_back({ LAYER_3.data(), LAYER_3.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_3.data(), PAIR_DIFF_INV_3.size() }); + layer_hex.push_back({ LAYER_4.data(), LAYER_4.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_4.data(), PAIR_DIFF_INV_4.size() }); + layer_hex.push_back({ LAYER_5.data(), LAYER_5.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_5.data(), PAIR_DIFF_INV_5.size() }); + layer_hex.push_back({ LAYER_6.data(), LAYER_6.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_6.data(), PAIR_DIFF_INV_6.size() }); + layer_hex.push_back({ LAYER_7.data(), LAYER_7.size() }); + diff_inv_hex.push_back({ PAIR_DIFF_INV_7.data(), PAIR_DIFF_INV_7.size() }); + layer_hex.push_back({ LAYER_8.data(), LAYER_8.size() }); + return EcfftDomain::from_hex_arrays(domain_data::LOG_N, layer_hex, diff_inv_hex); + }; + + auto domain = build_domain(); + size_t n = domain.levels[0].size(); // 256 + size_t degree_bound = n; + size_t num_queries = 4; // small for test speed; per-query cost is representative + + // Random SRS encoding + std::vector g0(n); + for (size_t i = 0; i < n; i++) { + g0[i] = bb::grumpkin::g1::element::random_element(&engine).normalize(); + } + + // === Native prove === + auto prover_transcript = std::make_shared(); + prove(g0, domain, degree_bound, num_queries, prover_transcript); + auto native_proof = prover_transcript->export_proof(); + + // === Native verify (sanity check) === + auto native_verifier_transcript = std::make_shared(native_proof); + bool native_ok = verify(domain, degree_bound, num_queries, native_verifier_transcript); + ASSERT_TRUE(native_ok); + + // === Build recursive verifier circuit === + Builder builder; + + RecursiveBaseFoldVerifier::verify(builder, domain, degree_bound, num_queries, native_proof); + + builder.finalize_circuit(/*ensure_nonzero=*/false); + size_t num_gates = builder.get_num_finalized_gates(); + + info("=== Concrete Recursive Verifier (log_n=8, ", num_queries, " queries, ", domain.num_rounds, " rounds) ==="); + info(" Gates: ", num_gates); + info(" Gates per query: ", num_gates / num_queries); + info(" Gates per query per round: ", num_gates / num_queries / domain.num_rounds); + info(""); + + // Extrapolate to production parameters (log_n=18, 43 queries, 18 rounds) + // Per-round fold cost is representative; Merkle cost scales with depth. + // We can't perfectly extrapolate Merkle (different depths), but fold dominates. + info(" --- Extrapolation to 2^15 MSM (blowup 8, 43 queries, 18 rounds) ---"); + info(" NOTE: log_n=8 has 8 rounds; log_n=18 has 18 rounds."); + info(" Per-query cost here (8 rounds): ", num_gates / num_queries); + info(" Estimated per-query at 18 rounds: ", (num_gates / num_queries) * 18 / domain.num_rounds); + size_t estimated_total = (num_gates / num_queries) * 18 / domain.num_rounds * 43; + info(" Estimated total (43 queries × 18 rounds): ", estimated_total); + info(" Log2: ", std::log2(static_cast(estimated_total))); + + // Check circuit is valid (this may be slow for large circuits) + // Only do this for small query counts + if (num_queries <= 4) { + bool circuit_ok = bb::CircuitChecker::check(builder); + info(" Circuit check: ", circuit_ok ? "PASS" : "FAIL"); + EXPECT_TRUE(circuit_ok); + } +} + +/** + * @brief Helper: load or generate an ECFFT domain, prove, build recursive verifier circuit, report gates. + */ +void run_full_size_benchmark(size_t log_domain, size_t num_queries) +{ + using namespace bb::basefold; + auto& engine = bb::numeric::get_debug_randomness(); + + std::string basefold_dir = std::string(std::getenv("BUILD_DIR") ? std::getenv("BUILD_DIR") : ".") + + "/../src/barretenberg/msm_verification/basefold"; + std::string domain_path = basefold_dir + "/ecfft_domain_2_" + std::to_string(log_domain) + ".bin"; + std::string script_path = basefold_dir + "/ecfft_precompute.py"; + + // Generate if missing + { + std::ifstream check(domain_path); + if (!check.good()) { + info("Domain binary not found at ", domain_path); + info("Generating (may take several minutes for large domains)..."); + std::string cmd = + "python3 " + script_path + " --log-n " + std::to_string(log_domain) + " --output-bin " + domain_path; + int rc = std::system(cmd.c_str()); // NOLINT(cert-env33-c) + if (rc != 0) { + info("Generation failed (rc=", rc, "). Skipping."); + GTEST_SKIP() << "Could not generate domain binary"; + return; + } + } + } + + EcfftDomain domain; + try { + domain = EcfftDomain::load_binary(domain_path); + } catch (const std::exception& e) { + info("Failed to load domain: ", e.what()); + GTEST_SKIP() << "Domain binary invalid"; + return; + } + + size_t n = domain.levels[0].size(); + size_t degree_bound = n; + size_t blowup = n / (1 << LOG_MSM_SIZE); + size_t blowup_bits = static_cast(std::log2(static_cast(blowup))); + + info("=== Full-Size Recursive Verifier ==="); + info(" MSM size: 2^", LOG_MSM_SIZE); + info(" Blowup: ", blowup, " = 2^", blowup_bits); + info(" Domain: 2^", log_domain, " = ", n); + info(" Rounds: ", domain.num_rounds); + info(" Queries: ", num_queries); + info(" Security: ~", num_queries * blowup_bits, " bits"); + info(" Generating random oracle..."); + + std::vector g0(n); + for (size_t i = 0; i < n; i++) { + g0[i] = bb::grumpkin::g1::element::random_element(&engine).normalize(); + } + + info(" Proving..."); + auto prover_transcript = std::make_shared(); + prove(g0, domain, degree_bound, num_queries, prover_transcript); + auto native_proof = prover_transcript->export_proof(); + info(" Proof size: ", native_proof.size(), " Fr = ", native_proof.size() * 32 / 1024, " KiB"); + + info(" Building recursive verifier circuit..."); + Builder builder; + RecursiveBaseFoldVerifier::verify(builder, domain, degree_bound, num_queries, native_proof); + + builder.finalize_circuit(/*ensure_nonzero=*/false); + size_t num_gates = builder.get_num_finalized_gates(); + + info(" === RESULTS ==="); + info(" Total gates: ", num_gates); + info(" Log2(gates): ", std::log2(static_cast(num_gates))); + info(" Gates per query: ", num_gates / num_queries); + info(" Gates per query per round: ", num_gates / num_queries / domain.num_rounds); +} + +// N=2^15 MSM, blowup 8 = 2^3, domain 2^18, 43 queries (~129-bit security) +TEST_F(BaseFoldCircuitCostTest, FullSizeRecursiveVerifier) +{ + run_full_size_benchmark(/*log_domain=*/18, /*num_queries=*/43); +} + +// N=2^15 MSM, blowup 16 = 2^4, domain 2^19, 32 queries (~128-bit security) +TEST_F(BaseFoldCircuitCostTest, FullSizeRecursiveVerifierBlowup16) +{ + run_full_size_benchmark(/*log_domain=*/19, /*num_queries=*/32); +} + +// N=2^15 MSM, blowup 32 = 2^5, domain 2^20, 26 queries (~130-bit security) +TEST_F(BaseFoldCircuitCostTest, FullSizeRecursiveVerifierBlowup32) +{ + run_full_size_benchmark(/*log_domain=*/20, /*num_queries=*/26); +} + +} // anonymous namespace diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain.cpp b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain.cpp new file mode 100644 index 000000000000..6f4566702bee --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain.cpp @@ -0,0 +1,108 @@ +#include "ecfft_domain.hpp" +#include "barretenberg/common/assert.hpp" +#include +#include + +namespace bb::basefold { + +namespace { + +Fq fq_from_hex(const char* hex_str) +{ + // Parse 0x-prefixed 64-char hex string (big-endian) into Fq + // Fq::serialize_from_buffer expects 32 bytes big-endian + std::string s(hex_str); + if (s.substr(0, 2) == "0x" || s.substr(0, 2) == "0X") { + s = s.substr(2); + } + BB_ASSERT(s.size() == 64); + + uint8_t buf[32]; + for (size_t i = 0; i < 32; i++) { + auto byte_str = s.substr(i * 2, 2); + buf[i] = static_cast(std::stoul(byte_str, nullptr, 16)); + } + return Fq::serialize_from_buffer(buf); +} + +} // anonymous namespace + +EcfftDomain EcfftDomain::from_hex_arrays(size_t log_n, + const std::vector>& layer_hex, + const std::vector>& diff_inv_hex) +{ + EcfftDomain domain; + domain.log_n = log_n; + domain.num_rounds = log_n; + domain.levels.resize(log_n + 1); + + for (size_t i = 0; i <= log_n; i++) { + auto [hex_ptrs, sz] = layer_hex[i]; + domain.levels[i].domain.resize(sz); + for (size_t j = 0; j < sz; j++) { + domain.levels[i].domain[j] = fq_from_hex(hex_ptrs[j]); + } + } + + for (size_t i = 0; i < log_n; i++) { + auto [hex_ptrs, sz] = diff_inv_hex[i]; + domain.levels[i].pair_diff_inv.resize(sz); + for (size_t j = 0; j < sz; j++) { + domain.levels[i].pair_diff_inv[j] = fq_from_hex(hex_ptrs[j]); + } + } + + return domain; +} + +EcfftDomain EcfftDomain::load_binary(const std::string& path) +{ + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open ECFFT domain file: " + path); + } + + auto read_u32 = [&]() -> uint32_t { + uint32_t val; + file.read(reinterpret_cast(&val), sizeof(val)); + return val; + }; + + auto read_fq = [&]() -> Fq { + // Read 4 x uint64 little-endian (matching Python export format) + uint64_t limbs[4]; + for (auto& limb : limbs) { + file.read(reinterpret_cast(&limb), sizeof(limb)); + } + // Construct from Montgomery form limbs + return Fq(limbs[0], limbs[1], limbs[2], limbs[3]); + }; + + uint32_t log_n = read_u32(); + [[maybe_unused]] uint32_t n = read_u32(); + uint32_t num_rounds_val = read_u32(); + + EcfftDomain domain; + domain.log_n = log_n; + domain.num_rounds = num_rounds_val; + domain.levels.resize(num_rounds_val + 1); + + for (size_t i = 0; i <= num_rounds_val; i++) { + uint32_t m = read_u32(); + domain.levels[i].domain.resize(m); + for (size_t j = 0; j < m; j++) { + domain.levels[i].domain[j] = read_fq(); + } + if (i < num_rounds_val) { + size_t half = m / 2; + domain.levels[i].pair_diff_inv.resize(half); + for (size_t j = 0; j < half; j++) { + domain.levels[i].pair_diff_inv[j] = read_fq(); + } + } + } + + return domain; +} + +} // namespace bb::basefold diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain.hpp b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain.hpp new file mode 100644 index 000000000000..11bc87aba52e --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain.hpp @@ -0,0 +1,221 @@ +#pragma once +/** + * @file ecfft_domain.hpp + * @brief ECFFT domain hierarchy for FRI-style folding over non-FFT-friendly fields. + * + * # Background + * + * Standard FRI uses domains {ω^i} in a field with large 2-adic roots of unity and folds + * by pairing ω^i with ω^{i+n/2} = -ω^i. BN254's base field Fq (= Grumpkin scalar field) + * has 2-adicity 1, so standard FRI doesn't work. + * + * Instead we use the ECFFT Part II construction (BSCKL22, Appendix B.2): a chain of + * 2-to-1 rational maps (isogenies between elliptic curves over Fq) that gives us a + * "binary tree" of domain points: + * + * L_0 → L_1 → L_2 → ... → L_k (|L_i| = n/2^i) + * ψ_0 ψ_1 ψ_{k-1} + * + * Each ψ_i maps two points in L_i to one point in L_{i+1}. The pairing invariant is: + * + * ψ_i(L_i[j]) = ψ_i(L_i[j + m/2]) = L_{i+1}[j] + * + * where m = |L_i|. This is first-half/second-half pairing, NOT even/odd. + * + * # The pointwise FRI fold formula + * + * For our isogenies, v(x) = x (the "denominator function" from ECFFT Part II). + * Given a pair (s_0, s_1) = (L_i[j], L_i[j + m/2]) with degree bound d and + * verifier challenge z ∈ Fq: + * + * e = d/2 - 1 ("degree-aware" normalization exponent) + * a = f(s_0) · s_0^{-e} (normalize by v(s_0)^e = s_0^e) + * b = f(s_1) · s_1^{-e} + * slope = (b - a) / (s_1 - s_0) (interpolation slope) + * fold_z(f)(t) = a + slope · (z - s_0) + * + * where t = ψ(s_0) = ψ(s_1) = L_{i+1}[j]. + * + * This is POINTWISE: each output depends on exactly 2 inputs, enabling O(1)-per-query + * verification (vs the O(n) global ECFFT Part I decomposition). + * + * # Type genericity + * + * fold_pair is templated so T can be: + * - Fq (scalar FRI over the field) + * - GroupElement (group-valued BaseFold: the · operations become scalar-mul on curve points) + * + * In the group case, "f(s_0) · s_0^{-e}" means multiplying a Grumpkin curve point by + * the field element s_0^{-e}. This costs one elliptic-curve scalar multiplication. + * + * # Precomputed data + * + * We store: + * - domain[j] for each layer (the actual Fq elements in L_i) + * - pair_diff_inv[j] = 1/(s_1 - s_0) for each pair (avoids a field inversion per query) + * + * These are generated by ecfft_precompute.py from the ECFFT domain construction over Fq. + */ + +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include +#include +#include + +namespace bb::basefold { + +// The ECFFT domain lives in the Grumpkin scalar field = BN254 base field. +// +// IMPORTANT naming pitfall: +// grumpkin::fq = bb::fr (BN254 scalar field) ← NOT what we want +// bb::fq (BN254 base field) ← this is the Grumpkin scalar field +// +// We use bb::fq because our domain points are elements of BN254 Fq, which is the scalar +// field of the Grumpkin curve. Scalar multiplication on Grumpkin points (GroupElement * Fq) +// requires Fq to be the Grumpkin scalar field, which is bb::fq. Using grumpkin::fq would +// give bb::fr (wrong field) and fail to compile. +using Fq = bb::fq; + +/** + * @brief One layer of the ECFFT domain hierarchy. + * + * Layer i has |L_i| = n/2^i domain points in Fq. + * The first half and second half are ψ-paired: ψ(domain[j]) = ψ(domain[j + m/2]). + */ +struct EcfftDomainLevel { + std::vector domain; ///< L_i: domain points, size m + std::vector pair_diff_inv; ///< 1/(L_i[j + m/2] - L_i[j]) for each pair j, size m/2 + + size_t size() const { return domain.size(); } + size_t num_pairs() const { return domain.size() / 2; } + + /** + * @brief Get the paired domain points (s_0, s_1) at pair index j. + */ + std::pair pair_at(size_t j) const + { + size_t half = domain.size() / 2; + return { domain[j], domain[j + half] }; + } +}; + +/** + * @brief Complete ECFFT domain for FRI folding. + * + * Contains num_rounds + 1 levels: levels[0] is the initial domain L_0 of size n, + * and levels[num_rounds] is the final domain of size 1. + */ +struct EcfftDomain { + size_t log_n; ///< log2(|L_0|) + size_t num_rounds; ///< number of folding rounds (= log_n) + std::vector levels; ///< levels[0..num_rounds]; levels[i].size() = n / 2^i + + /** + * @brief Compute the fold coefficients α, β for a single pair. + * + * The fold formula reduces to a linear combination of the two opened values: + * + * fold(f_s0, f_s1) = f_s0 · α + f_s1 · β + * + * where α, β ∈ Fq depend only on the domain points (s0, s1), the degree bound, + * and the verifier challenge z. Derivation: + * + * a = f_s0 · s0^{-e}, b = f_s1 · s1^{-e} (normalization) + * slope = (b - a) / (s1 - s0) (interpolation) + * result = a + slope · (z - s0) (evaluate at z) + * = f_s0 · [s0^{-e} · (s1 - z) / (s1 - s0)] + * + f_s1 · [s1^{-e} · (z - s0) / (s1 - s0)] + * + * So: + * α = s0^{-e} · (s1 - z) · diff_inv + * β = s1^{-e} · (z - s0) · diff_inv + * + * When e = 0: α = (s1 - z) · diff_inv, β = (z - s0) · diff_inv. + * + * This formulation is critical for circuit efficiency: it reduces the fold check + * from 4 group scalar multiplications to 2 (plus cheap field arithmetic for α, β). + * See OPTIMIZATIONS.md, Optimization 1 for details. + * + * @return (α, β) such that fold(f_s0, f_s1) = f_s0 · α + f_s1 · β. + */ + /** + * @brief Compute the folded value for a single pair opening (the core verifier check). + * + * Given the pair (f(s_0), f(s_1)) at pair index j in round `round_idx`, with + * current degree bound `degree_bound` and verifier challenge z, computes the + * expected value at L_{round_idx+1}[j] using the ECFFT Part II fold formula. + * + * @tparam T Fq for scalar FRI, or GroupElement for group-valued BaseFold. + * Must support: T - T, T * Fq, T + T. + * + * @param round_idx Which round (0-indexed). Determines the domain layer. + * @param degree_bound Current degree bound d. Halves each round. Controls the + * normalization exponent e = d/2 - 1. + * @param j Pair index within the layer (0 ≤ j < m/2). + * @param f_s0 Oracle value at L_i[j] (the "left" element of the pair). + * @param f_s1 Oracle value at L_i[j + m/2] (the "right" element of the pair). + * @param z Verifier's fold challenge for this round. + * + * @return The expected folded value at L_{i+1}[j]. + * + * When T = GroupElement, each · is a Grumpkin scalar multiplication: + * - e > 0: 4 scalar muls (normalize both inputs, compute slope, evaluate at z) + * - e = 0: 2 scalar muls (no normalization needed, just slope + evaluate) + * + * Note on circuit cost: algebraically, the fold can be rewritten as + * fold = f_s0 · α + f_s1 · β (see OPTIMIZATIONS.md, Opt 1) + * which looks like 2 scalar muls. However, α and β depend on the witness + * challenge z, making them witness bigfield values. In the current formulation, + * 3 of the 4 scalar muls use CONSTANT scalars (s0^{-e}, s1^{-e}, diff_inv) which + * cycle_group handles more cheaply. Benchmarking shows the 4-mul form at ~6,500 + * gates outperforms the 2-mul α,β form at ~10,200 gates because the bigfield + * arithmetic for computing witness α,β costs ~5,100 gates. + */ + template + T fold_pair(size_t round_idx, size_t degree_bound, size_t j, const T& f_s0, const T& f_s1, const Fq& z) const + { + const auto& level = levels[round_idx]; + size_t half = level.size() / 2; + size_t e = degree_bound / 2 - 1; + + Fq s0 = level.domain[j]; + Fq s1 = level.domain[j + half]; + Fq diff_inv = level.pair_diff_inv[j]; // precomputed 1/(s1 - s0) + + if (e == 0) { + // degree_bound == 2: no normalization needed, just linear interpolation. + // slope = (f_s1 - f_s0) / (s1 - s0) [1 scalar mul: witness × constant] + // result = f_s0 + slope · (z - s0) [1 scalar mul: witness × witness] + T slope = (f_s1 - f_s0) * diff_inv; + return f_s0 + slope * (z - s0); + } + + // General case: normalize by s^{-e}, then interpolate. + // a = f_s0 · s0^{-e} [1 scalar mul: witness × constant] + // b = f_s1 · s1^{-e} [1 scalar mul: witness × constant] + // slope = (b - a) · (s1 - s0)^{-1} [1 scalar mul: witness × constant] + // result = a + slope · (z - s0) [1 scalar mul: witness × witness] + Fq s0_e_inv = s0.pow(e).invert(); + Fq s1_e_inv = s1.pow(e).invert(); + + T a = f_s0 * s0_e_inv; + T b = f_s1 * s1_e_inv; + T slope = (b - a) * diff_inv; + return a + slope * (z - s0); + } + + /** @brief Load domain from a binary file (generated by ecfft_precompute.py --export-binary). */ + static EcfftDomain load_binary(const std::string& path); + + /** + * @brief Build domain from hardcoded hex string arrays (generated by ecfft_precompute.py). + * + * Used for compile-time embedding of small test domains (e.g. log_n=8). + * For production (log_n=18), prefer load_binary(). + */ + static EcfftDomain from_hex_arrays(size_t log_n, + const std::vector>& layer_hex, + const std::vector>& diff_inv_hex); +}; + +} // namespace bb::basefold diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain_data_2_8.hpp b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain_data_2_8.hpp new file mode 100644 index 000000000000..88ef5f356e9c --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_domain_data_2_8.hpp @@ -0,0 +1,841 @@ +#pragma once +// AUTO-GENERATED by ecfft_precompute.py +// Domain size: 2^8 = 256 +// Rounds: 8 + +#include +#include + +namespace bb::basefold::domain_data { + +static constexpr size_t LOG_N = 8; +static constexpr size_t N = 256; +static constexpr size_t NUM_ROUNDS = 8; + +// Layer 0: size 256 +static constexpr std::array LAYER_0 = { + "0x0cdfe205cc1f7394d2e36ee07fac36ce66e7feeac01c636f51a2e3f94fd47148", + "0x1b16b4caf4e13cd0b2a9052aec5d8d6ab26d37fe9e63a8af2f5e07f488c22852", + "0x16cb9a2a977b4498ed9ff544ef7aca8ea1f07ba8ae3fdaf4c60611b1ea0fedfa", + "0x0748ed2088f66b6ac8a17591870796f4b8cf1771cabf903593661c1fa469a7f4", + "0x08d64d76a988470e3c9488833ce3bbfa374e1a313c09e8d3458a53c7090a84ce", + "0x2c524981a67c0a4674477d3c31a81cfdc5bb26ffb585bb53e75913a70871b823", + "0x198f5c11ec62c9fe2a8acadb4a1e841736613731faef77a790ce600e3412db93", + "0x07831dd4d486f74e8c4c088a4c193db22c04ca0e5b33cc8c7f741f09eadfd2b5", + "0x1f5bd28d3a567f2dfb06ada1bf140132012193ee40d3c5ab73a36a1c78bde0cc", + "0x1aeba8eb7f0edfb0fdcfa477ab46375d4317c97093e55d6bc993ceea84e8f03f", + "0x0bcefd714348f14929e055b816fde076d14f25c445aa6f49227454aed25b5541", + "0x069bccec91b351443bd9c3d7123ee97f2b93101dfefbb579f0a8a60ef964937c", + "0x2ce61974feb6af4472f42506aaa885bc0c735376c1b9763dd41c8138b59d9b93", + "0x15a0e6d0bbc06d6b8ed7402c68573200632f6e68bc146591be7909160627e318", + "0x21882a43981405958fe177dd7ea7121c64803de6b8e66ea6c9c6268783573aff", + "0x13cb3381935ac7e4ceb381bc79fe90edf4d2bcac1dd674640092851bb53c7295", + "0x185c84056ec476ad1516ae3b3fc147218990eba2f47b75cee9dda5b7c8fd97a0", + "0x1a8f216b4340d82e05c7c411113c2efef0bfdfab8476344c6d5444232fae486d", + "0x04b0c67dac7fd9260c1df4fd163cf571e918107c43cddfe132d5d0471b2f288f", + "0x2efdc55b5a75ba510407bb282fa12f239c4af4ccd443966491878df9cb3c45a0", + "0x11a204a8d1560278ba5952ce58a7be970fb552c2be202c8da6c65267a7980954", + "0x0c08add47aaf7cde1b609b76ea463490e64a609447a1fab3055a2d0a848c490c", + "0x1b344ba695a85f04cd8471d7adae1d5054c1daf081fe818f255595d07f5bbce5", + "0x0108f8814e5cb7c74d3469458972008d1847b2d732e3f1ec35c7d3acb5d5f7a9", + "0x2bebdeddef1e3df6ca6b0e239f66bf3b5c2f21c702a857898d27858488846967", + "0x0889efbd4d09921c9b61508a1a6bc0dcdf0cbccfbd63c6c4a0c844fe97728d6e", + "0x0717aef74ec49480bfd8e2930e10ff37e999dacef42bd432b0f5504c2122af2d", + "0x2cb7a904b713f736971c580c5930ea0d4de44268ab8c3955c891d8650bb92260", + "0x14f15f5f7412239eb5d905619a00cc1010708074d785c6c1fb6e1b0d3e63c441", + "0x2621c27c36596655fe98c2687b8d770c2a0a6df1e0053e08bf52c03b253bb94b", + "0x020d159e99316b81865581b9f6529cf398f394fc9f9857915c9a12c4f7505966", + "0x0be67a9119b1073ff7edd6a313e6904ebe1c738e8b16fd412c5cd50d8a88de09", + "0x2fa964a323728017a7031504aa80e8d3678b00f16680c33f3d42ec9042ffc12f", + "0x1119cb5453936194278652f9ea14c17f91dece71866df2d7f16fc2c33bf007a5", + "0x06fd09ffdadb3bb19eb9d2311d759b8cef9666c8999ca726e538db4b8b746fb6", + "0x2d99f2018184028f6f4e19155e88cfbf976a5d627c4812bf8af4ef4efd089418", + "0x1ace83e110f72b94fd22ce59dc2af7f939f5e665b7e48bead0d01f1b9abef73a", + "0x0d9cdea998103075a2418815db14e35fe4f74890fa402eda56c86cee4c43e0a0", + "0x300b7ee311a72ce3c87b0241903c08fe8529cdcc53fc517aaf2628080ff7e19f", + "0x00d53212ad590464806d897b10d915480e7e20c16055d337a0b7c80b38c589d6", + "0x11c86c7eeecdbf14d0217cf5c3f8d1a4412e7d34e8b8e0b85f5e7e2c5fe049c6", + "0x27c46a9dd50f009ceb49c3bcd4d471cfe84b2c1e27666c233e7b39163052ca22", + "0x0af2748f493b678dd373a278c054ac9478fba7a7c7939c9b9c5f45ddeba62483", + "0x013ceee086eedba523940133e821338b39a64be53ac0b507b4da4148ea6c6f99", + "0x0f35df4b931e6bdc5673359da9d65ec045f1241d82181273e63abb1e7558da5c", + "0x09d2d5e181a93ebae735ab14320b11cba079979e65967226eb8c635c80e8a122", + "0x0af7a1220932e008e5708737fa4419bafbf545fc8b7b74de402ea670a9bfd219", + "0x058d9f85dbc14e47d4383c9eccbe4b051b84d9324fa3ab3011dcafa9c7707e31", + "0x0ecc96db9c110010a746182fe42e6d1d47594e4a2feb5baf86048c0ae2ce59ab", + "0x0f396032edb017cc8091d1121a7e24b96857d59be265f29b2a48abc7334718de", + "0x0ea503f55c3ec2f75403c0eb0edd1190dc3ab6bbd2089e8755ced33550fd6427", + "0x124dbc25d4c0a8b8de9e78aeebec4e42297cc94dc022ccbfbc053238f3e6a1ec", + "0x2f80edd3bfe7a541ff27c096f70b663680bc8aaa1a0688647ec7f012b23101a4", + "0x2511d9cdd074998d02149dd1d90a894e366960ee567652ef6f8401ab123125da", + "0x2e5a3731752f0ec1c21d56b16a274d6887aa830c215b8a28cf7858b3ef3e7841", + "0x258bdf926600957f9757f4c3d3ee075bffdff70264f93941d71713b19cd09d19", + "0x0c6023a24663aaba15c066ae3d4eca64d6dd7b9195c1b03c3d36388259052332", + "0x2143fc94fa8e384fb7e4d64068691e50b81ea03a2f4d394a6ef8607de0ec2d67", + "0x240288d71b640417ae319e6f0b99c2d203d261d48d4621f4a675ea33c718d33f", + "0x14302f33d053ed3d48d21b87498f29b79948906d42a288ef4e900d63557b9202", + "0x29021b32638e14f6bebaf3a5d9e60af275051a91f796726bbb14190fefee6413", + "0x00f8c1133ca0d6a0d15c63066b9c9f8c7083103c68b862396c7b81ad9c9a96e0", + "0x11d9c5b3a31d94e8b615faa8e5eebb950c5023730ee081678071fab5d56a46c4", + "0x23504a347f851ed87b7994a685c4fc1853f6e3cf59c5491d758d10f552dbc186", + "0x171b6f4a3e065bf52d0ccbcc805e1261b45efdb1916bde6f84a09c48c3eff9d9", + "0x20d8948665b4040ae8270eac89d449ad3979ce040d5e622ebff258877c90ff5c", + "0x1d57a30fcf85a4a330c44354c96f445608fe37063f8375f776f0222952723047", + "0x11a9faf1838d336f5fa234fae033b2ed5dda2d53ef760c5b2e9008b3f3f578b0", + "0x0575057c29ba9235bc826983bf771cd5c24dfa30460949310db0e08467a745fd", + "0x23783ba5e5f363527202d4d4fc3c75c762c6c8914f3c318a05bc19cc7328d8da", + "0x2c68549b9fc9526bf807019e93f728823d289d8210be855e79a69a609d0ec7bb", + "0x207eab97ad951e45a346d9898925e95bab42bf490a8ab405d1bdd6e9e93c035c", + "0x20d1ee56ea060bc3a378f4abfdf52e1b514d6450758bca00f9288257cccb3069", + "0x24432a988288b76818ae9d257aaa298b2e2459cf44157f7493d0989b1f84c0d7", + "0x0ab2bc6a0048bc2ca80bd6d7081504466fb636be09c912c3fee4155b1aadb408", + "0x2984a03b8b11cbbb6d653c24969f565124cb34c2c401c12a026640d0ba8bbb54", + "0x11d6099dfdfc3d72efbd2ac64693d04920cf29d2c02fe37876c82c94af1e3e6c", + "0x17833a0e6ee5e0b82d4ee356631f941b93bd7e7a8e68714c30f23fccd38265c6", + "0x0a7de65a20fb6b9af3f5df1d7015e6e1dec5ea5068810e8e7607452087097284", + "0x2e28d533864ca4eaf34f92f96d0cec5c3c41d9ea1df320aab4833e2b0403d0e9", + "0x1adfb78110dfacef79b3ce070c5bb875426da0ba0cfce737263906721de330be", + "0x22a242cf730bf2ad07cf45c9ccf3258f4ca1922a7f3a22f5988c76bb17aa88bd", + "0x2e2fb2be0fb08372dcc31c1973980f6659b934a2a9fe4c688dbd284c8835df51", + "0x2b03774c767f6e9d4782abf2b6791e3b6c6cf7ff5798709a528f24bcbf1b0778", + "0x009516579ca3c7695cfa88c35210b4c72d493e78418749edd92ac68c06ca5744", + "0x087c29a5f44b2fe4bd90b7ebbba1557f6dbcb517cd747ebc18fe08cc28963561", + "0x08bceb53131317a36f8d17af003f400ddd05612e7ee1831dd1b5b59fe6361ac9", + "0x0f0750e7852ca083650cbea9d4228f37ce15041131c526deea934a9ec1f633e0", + "0x2cb2b26e8da025f10b918da2793d64bc2d20cccfc0203ad771c0c841481fe71a", + "0x0b228eb360d49e9ae1233497aedfcd17d40092899ead249bdab066fa620e3f6e", + "0x1597492f7d232c73b90a974e45911b69d43bd8d29554412ced7497598ed900c8", + "0x0f6fbf5b244e495e333de87379b1c74b500b25cde450f94734a0c6845ed583ec", + "0x1785202030dfd611e3c01e5372c39f7b9a428fb04ea5db87343931ccac637bb0", + "0x0b110012868173679476fa2274902732881944e99394c9eb91aaddf0db1da763", + "0x06485f35a297f2377c71171cad430cd47c31b8ad56a5282badfdeff4470b03f7", + "0x2c80356a2287dc94786aa960ab545970aa6070474a20e3e8606f89cb5ae6a6f0", + "0x29f551489bb43eb44312e44ee316eb7e4400b26ea3eb0653b7cd3f56f302ca99", + "0x04c20be9c2a98e9d8fdead1643904810176e9e61eb827b5d3d9cc4b077f1c1c7", + "0x11f9232e02811040746a96e91c2c02e8988aa30b40d36c2977437bc6773fb0d8", + "0x0d74259c2f9fed2131642d8c11d6c33c2e6a493bab7f1e4622b187056c949d05", + "0x2b0afaeb0884b5421cdf60c600fb5c44e7927e77faf32b325c3c972436134c3f", + "0x295af6d3fac215857a54330eb52eb7f4643b05ceac642dea56ba63cc29954b63", + "0x0bb9b23978f4478aeda51d599c8c90f951ede3f287c495bdcd370cce8222ad46", + "0x052e75a98e80353dc19dc01021d70ad1cb4222c1e140b6347994243bba80151a", + "0x0a5f3d1787c5c4f0a780dde5b7f029b52c0ce0dc4e075a8decdd140c42e144b4", + "0x24caa6f728ebc814b8ab11bd56af121883c60fda6d3f21fbf2b603446cbe1583", + "0x0350237e1461ca987faeaeb24aa3ca3a70f3e8d7deb6b03d425ad00b8a92acea", + "0x0f5aae3b4e23de9a3e594c133445e1c706feeb3e6ea00bef15f5eb9b33ea5963", + "0x2c57ecf41ee5cc166dff1117ab7e2a78454e4695c3aeda734d27242bf313c817", + "0x1bc7cc9fc37b10caadf7a8b13026d8de06ae2639faaa11b2c8c321db01293321", + "0x2ae7a4ca583fa77e8e1e17df93ac7b8c3538c08c4cf769aff17e048a2041f9c9", + "0x0ee6a27158c5e1d43827c182b304d9c0c02158081fc78c50b8761c5231f8611a", + "0x200c0569e248e4f157a68caa95fd560bf60f57a58e028e99af036b5068a32453", + "0x25cfeae7986d78c48e74014adf6326c5125c20466cf96289dcfef29c8a2771fe", + "0x1845cedec21d48246d3f9e1b017777a4d5d30fc44b444451dd3b963cdb1cb198", + "0x2826be6f7960c1fbf84f84bd2aba56346ab15e440d972fde4ace6885a6087121", + "0x2da4d4797d440b847a4ecc3eb5f870530957291d6750c6005dcf094f602ed861", + "0x0c17a86cf252ed7f894cfdd6f20db284a82cf6282656eafbd435bb6729f9a764", + "0x1866d1af194dd56c7739fe5f460267b3517a961f84bb896425ce8f2041aa8d44", + "0x0623844f43290b4b70a27b3bb8fadac00ac30ee11f1895a29f8c6a41ab8b6fd7", + "0x0f223cfc968609f7a398ce2c9be4cf63f7bea73ca7c61756e4d60c3c8ac4c41c", + "0x2d9fee188a908506c7e7d55a6cb60fbf965f046c1a0d92d483ff8d58104c2dad", + "0x2bef3201226ec819df5a1ecf768ca442ee00a7b35ce18b2d927fbf3f7111871b", + "0x23d054b4b288393ef2b68ca4b2b280a6fe5d8db52710bf289f64171b83173820", + "0x212bd290780f3b29be3e0ba902eb01e232ec5f419da1d8f554557a4ecfdcad4a", + "0x2c774bd137e1369d87c8ae89bc985cbbfc5563ffe4f050330f8ccdad834e01ee", + "0x196a5ac0c686da60e8ea154f79561054e8267729b81d88f51a06bca61655a88d", + "0x03fc8025bae04007d45d96f843983330c13a59eb6bd6bde249ce2b2509dc1e9d", + "0x1aec3024a178657074d9d6c2e5abaeff4264a32f04e4af9739c79816d1074d0f", + "0x0106cf222dd6f27dce2c337df737c73a1d37af21af5ee0678896cda6b6ac34ea", + "0x10ed62412cbce46e22af44bc9e2e4f7e822ecc7d814899876ff6748b44863acc", + "0x0927fd5f5593479417b91cc1f9588c9321507cbb2b47fc640105d3118af1857e", + "0x29afcc54a5767973446b8b81e098563d496d7ba0aa12c0abedb6d333c2a039b5", + "0x1f2b65817741d699be81473e129dd2fe7df42259016804580135ea827230a7e0", + "0x10b6dd5007db05652cb2dcbe364edbcc0873953ce4c303682108206f9d39c086", + "0x24d65700f714db4df5e72ea0b4783a7910ee337abf6cab47d2365c207a2a196b", + "0x0f9c3638427a27ba4531ae2df12e08755280c52d052dac42a643220721a6c496", + "0x2d3411aa108a96a75a23d6b23ee813448ee37322fcf0aba5aea49f2c6e82052f", + "0x08072366c16c26797d2571e0f3316604b0f3c5bd0764ac957f886bdde7f41b10", + "0x13407c0a632106413a2a26014afe059a33f7ba3204fa65ffa12e642e0de00f66", + "0x073327c75a5de56a16703302374dd8c6454637f7a8b16ab69a6bb6b6ed677e8c", + "0x0d3e91c7b0247fd60bbb7aee666e08e9c4ff70e0906265d1f7d2071fe2a5cffc", + "0x16a07bb2963cac2b3b45745f3fb82f2cbd99b63adc646be02915f4ef427b404a", + "0x0d73eb3f2f9c4227f2b02ba2d7295bb804db933dcc115d5c43537a08ec86fdc0", + "0x0c03acdf40dae8cff60be38aa2b27d86562c675216d2562a3324e6222c1825c3", + "0x1d3ffacdf5ec208974c8efd57f5042c431631aca2866ed44bffbd4b3b51d5670", + "0x24e560513780a85d5269e762c4f621a2d316b7d9a5929cd2867ace79b9ebc4d4", + "0x08ed51146b0e12a55c3b7f89e9d9091b3c891b40c73243f71987769bdaeae171", + "0x18343f6fceabe5e90486291ccc7e9883ee760a481204b7dbc17a8bc30bb00bc6", + "0x2dc8afa253ef48d3bc0d7ad6c48d41efea1e67ab22a0f15b563a9be0b980d5f8", + "0x1605d9270721ed8936ec1bb83d84769e15508a7e4a024d86da4ae842dab7378a", + "0x280b5caa95a0b7a0d01b5dea9a4a46baedc3d45a450cb32d426e8c5f86c633fa", + "0x2750767d4e69b4b91b9cbadb2d17826ac297cf50a9a03430dfc93ace0a48e10d", + "0x11ae009c3d9deb572c00b38f871f2cfd08963a66069ed66e759cdba1a85d194f", + "0x05597da8db6f277c2589f7887f3dd4fe78ec11d13738ff4e36a9cd781f750ddb", + "0x2cce7395814a93e482c4bff54da366445c2f5e6677a22b8f6ac019c424f34937", + "0x1b3cb1df3a1fb17a3a6f0be21b030169d383d122b1508d54c402ca7105515182", + "0x155e7090ecc1a585fb7e00cb35812fcbcdbdb7cbad69fd1fb458e1f53e42a5e8", + "0x1722d9917cf02fa3b025f356ddba24c7eded28714dc4d91006d38dad111c0319", + "0x2ecd0526e2075d5587ab894dfd4c6bea05bcda7014803de72f60205b7689b508", + "0x2d0cbd140fca56ba78d3e1667d7f7848c806ecbc2416fac86139e8745e00ed5b", + "0x1a16b61b0de63e1d32c801048c0f6296d62b31b11349b764faafaf06f6b6301b", + "0x17a38d27e98e5857ae1b6f763ee09223d45851607ceda546ffbc59a6e613a72f", + "0x11b38024a65aeedd5ac62561eb983b022d5408b37ed87797dee10106c550d39f", + "0x01914aac10d640367342cedc5ac7269e6649b2893900083d6da6881e127a984c", + "0x281a7ac47fc297426ab9bccb4e9fdc043e09db68fbbafab48f3c7b796cb4b4b7", + "0x260a1d1a51c85c33ef2551527197cc28c12ce39d5e535fbe29a2962b5d86b83e", + "0x0b15c7b95953eeadb30c1b0862590453361be5b4d821362182762389551c7510", + "0x15aaebdc127c7aa2c1f365c209b69efe0fef5a0dda4ade1ae1ce9cdafd65681d", + "0x1c2a2393c5e59a8475d67335c1e02ccb79f533633fb658ca41aa39d5acf60f51", + "0x251c12d99b0019579b59580b7a363073791b260bd0f17a9ddec932377f70fde2", + "0x1cb3c253f34d60fb04340512d77cc82add602bfb9300e8b25c624288184b029f", + "0x0495666319d2671413e389996fb923e579fdbf7587da045bd2326066b87be7ac", + "0x26c3430136f5a32b410acceb5656e53e41aaf3bdcd16fff627d552b4950a4552", + "0x160be4f084e04a764efda8ee5f3ae6cca0135860161122b417a3a4b028a19164", + "0x120dd32d55a8b9d047da8327b6272488e6275a8dedf9ea105831f504873277e3", + "0x017e2db5ca70213346a97dd1d83702485d1dc4efb7861a2fc73071465e040601", + "0x26fde69f294d63d22dd1656c23b149293a4f673ac0eea6573618ff435486dc37", + "0x0b6555efb6cfb7546ef2148f3dd4c435373ec69d844b455104e4d28cbd6e08b8", + "0x1b2e61698133cc75e819e42fc135f98cb818ca14173d28ddcff86d12382cf28c", + "0x0b61327c6d79f2cf080f212a018b629e611397d06ca68edd6aa42781fb6ac03c", + "0x175dc641f922e27dfe5b4f6628cacf5da40dcb849a3f1f7b0a6c05bdec078e69", + "0x1655c3cf8f3b5207604ddf1ba482cb6f7861f2559a755395761c133305ddf93b", + "0x0ded96fce8a742f5a6b24d547ab0594c72967b3d10d903bc0f2ad03adc5737a7", + "0x0754c0668ff83a43bc0b854477c3ad07ebb7ec11128628ac838287708fccaf77", + "0x1ba3b0b2d8cd9e67cfc3ae8f63abd078d2fba8c53e8a58e838ad66dc2d7ab4ba", + "0x1a45c0e36d098a2d13f1e775f2869c8a0a1887fb586d03320f0b95ddb1ea28e5", + "0x1295f17c0393ab0088b7979811b9b41ca56760034a54681a69c3460d7f95e0d6", + "0x0a97ec7d778c50be451c56cd6a74b25b7d8c70a40fb168c50e2ed9360e3704f3", + "0x1eebec580fcfaa059ce0098a466465808ff329d2940b01b64ed1fc4ccc0e59d6", + "0x1c2f48f5c2a80ec6205092104319bb99c5b2e49507ffb3757440f97e1bac877f", + "0x0cba0f5914fedf7e03d6cd84c716d87a1220d5ae98fdef014f444215bd466da0", + "0x163cfdbc22b0cc4922d85b3e5440b3576cf7db259480aaa750551c6120d0ef27", + "0x0539b64fe94833e2bc77e479b6aedb8cd8cad8af8e0dc2627167ee26b2afd7d5", + "0x23d083312542332538246ee89d6b2214d6791c52bd3cee060b62d6ce8e4b82b5", + "0x2183fa10dd79652fe1a686c850f046ab347f2a0e021fb86b38f3f3ec94f7c095", + "0x15a581c7b718b6fc2163f1f21a98b19cba46a5f0c4ff373f296cb905b9271b7f", + "0x256591e92ec0256a2f56d5352584562f04449cef8d12f4d0e32de0728a90ec42", + "0x18859cae1c964b5236379f6166ee52c8246594775f5e2af00c868821fa4a12d8", + "0x2456404ede6985ffdacd8e9ee395457711a9abd9d590adeea78fab4bd1a712f1", + "0x003deeba99d1ab8442e4d35afe54f1e56dcd0a3d1b303d32bb17e3ee7ca1eec6", + "0x136fcb38c809dd4b68d728acecb9c25ea7d5a67a3fe2e02558957a34a6ff0ecb", + "0x155f050147dd0cee243f99ebb96a586bd9b0b722a82dfb8e4c6b5a6c41445c4b", + "0x1839595b98dc4357eae451d97c1e35699cb57ec163d3e40929261f2414eb5499", + "0x1cc0d64145f9d07e62672fac9f52883ded5e8247a50b744bc8461fb4901633ee", + "0x2563b92b341c29b247206c01bf6ebd98ff055d1b15da9e8b52a067ba2a5ff82a", + "0x0d6b33d5bd05544d8331aba8eda931deacf9edaed14f4ba603daf98740dd71c5", + "0x2d9d57484ef1647d613b80cfcba53e929f0c0aaac61ed98d34f28f99c4a72e07", + "0x0a39eef645222487be73c3b02a360859f5ffa9494d7dfaa96d8cc931ada7bd42", + "0x076cf9cc33d516581579718b2f55ebb5b3bca39a418615cbc3461fa0b0d14570", + "0x2acb9892bdac52479fbd37d42ec4073e6fe9dbfea10e3662166c2c8518f7f49a", + "0x180fb69c18d963a66585b649032a2a5a8741fc3c361da0b8f455096107e7518f", + "0x0a966558c3116630ce840ed7b0dca9894b34370f24fa2fdbf4346e26cf4c65a4", + "0x0b0d87bc0d57f62a82ab064715b2aea9c5fa9d2b28d61c7d8403d228096818e7", + "0x2c06db33fe940a9ec1682efdbc31fc86e285a5eeab4b42ecfa5c2dd166d52d22", + "0x20a1aa11ff0e2fc05994d48b5fc53c44528b85f2916b9e3d18f314315fc9d2f5", + "0x269644bffda2947a117383ec5292cb9eb1502a6317cbd04ba45aa70e5373dd58", + "0x05641de73024df8abb84e21531fee62f7828011364f912f25c4ded47d871743d", + "0x0c9a2c7c9d4673ef21c362e22108d47eb66896aff9de0cc3b1a9718c869ee75d", + "0x192837dd6a014f352a284cd66dfd056386077c0868f343590601b8daed2e89ae", + "0x1a83baea76f78d036a4eb0d1b01d6b9a220965ad552107d4c9a96467b82dc3d3", + "0x150774876da83e1a76c01818833c8223cdcb0083e70bbec225129291e24c1b4d", + "0x07ed346836d11f801de41ea59106f4b343f8c37db688f2769110b64c0d9d93d2", + "0x2e3fdec26e79c380054eb031c9c0e5d98d1b7d5c361f54cb52423b547ff5559a", + "0x1005ce0e2aac22b9f7d28cb7a822eb47a73e2cef465d562a3862e550a3205104", + "0x0e62753f84c8c1981f39a90824c765ebdb6f0ceaf155e70bb8bf9ecf477a09cb", + "0x154aea05e70f44d450b409acd1dea9aa3e0c969a88a239d5819f371f812389d9", + "0x042fdc4a68979f685adf78d4b403c82c6e2f5bc124daeca8274ffe5bfdf6eef5", + "0x08dc7bf4c9e28ef3c9ed0de4ca82e8d3bed2b2a2b5ddefd5cf5214586748abaf", + "0x303512d7bcaa869e47e0350b4d4d58599f449d08ad2e951f9518c7d34208c03c", + "0x2e269542ef38e4ba8543fa2d08a09c02f2bc16845d6ab85341e71b603223794a", + "0x168c032258f1eaa55378b5aef7e98e48eafa8d9ca735bd225cfad883648f7c3c", + "0x2a54fae466303b59490bffa40bb2dee739bf50d39202b94ee32a7b4fa293af99", + "0x040ac580418e13a9c8cfbf132287775c5291613fad41e730495600a3b6c66d6b", + "0x195f488750b23361107b7951d310512bddf48b65a056b5c83d0c0d790e5c95f0", + "0x170a2d5472ed685d3f163fbaceea28b90c9f75cd50aa4a4573736600ff795f41", + "0x08aaef33c775001b12e4c0733468bc2892b459e506297321ca8827266235e73e", + "0x1964c10a4c586491544026af2ea48d71d08f3cbf967f5185981c23ff1ad7ccee", + "0x2599a63e17903cd322f673e3df0925db62cd49dec0404677f78cc552b1f83e3e", + "0x02de0669be137ff39e16d65a959887a79333a5f6b26e7f12ae2b066f69258864", + "0x168d745cd3c1099d87bbeea261edc8baacc10e1b56c75615f8e6bf8ab77ce596", + "0x01da2aa3c47c51ba3826a9b2f2e9235e57e60a6be7c170b3165391fb43519dc9", + "0x1d49d8380c0ee26eeea7b55c67a40fa96653fa377cf60db23bd5d8ef0d2ef353", + "0x018714afd6912648aa35f0a4f20419019a20f85ba5908f2e3e2ac8ab192fbfcf", + "0x12478136f4e501afbf71ccd79b5a38462eaee513c69e19153789f1949e53e09d", + "0x2ba65951fb43b881dc00b64c8d6149fc9c10e0b95a33ae61e5622d56b7e4d62e", + "0x1637a2b7d74de1f2b56fb6d8b65a96236219b13255c757a4b0ae03023723aa40", + "0x155cabbc9465f3931f34b07b78555d86bebb0edf242e46890a4ad04d8b5f90d5", + "0x0a9b6b7400f32f6a515070e2a8ed366c3fed665b698a05cc0f7e3a3f6a8f30d4", + "0x0a7ca0bfd54ae4dce2450bc7d96c3c10e57fdd06693bb95e43f243d4e4caa339", + "0x135501f4ebbd7e66a9d827bebddc14f8c89724212927ffdc6f6ab3f9f1e0a7b5", + "0x12b2827817ee53140a71bac982131670515c11f79b6a981fa0818d3ddf7178ac", + "0x259247dacce75c22d3e155bc14fde13d461fca6712f3abd09577da94f782688a", + "0x1e030aaf686735c792f1a9ff5ea813379381253dfb5b3ab5e2e32ff463196933", + "0x0d77f7ce6cc535d1fd7b70c90835ddbd6c286a81e3c882063ec497d82da0f520", + "0x0b973cd40d258d56f89305da2cc078b149e556b3752d6ea22488dcc1b8815d34" +}; + +static constexpr std::array PAIR_DIFF_INV_0 = { + "0x18a039cc7176518d1b281f07d83c6e99df09e51fe849ddc3b5eb2c41a5d59305", + "0x1c4be512fcd0f5f8bef1bd8db77a5fe181cc129bf4fd712cb36b7cb996346a4a", + "0x017ad76495b07dcc5964680be2c6f1be2dd00eb4e321c4849266a0425b08f4a4", + "0x098d504fc2326f5602e85f8f65c7958036515922cf3d9462caa46f3f1eb892b6", + "0x0ba12e4fcb35701dbef7e9dffd49052fed3b953ffd1fd4c2efc0b4a9f26a6f27", + "0x17123ca8978fba60810827c5e22bd9e5ed1ba8e5bb471de5e963472a9766b454", + "0x2eec444c36db56cc1fe679d94b3595b83762f30c293fadcd2785082ef2bbc9db", + "0x0246fd1c53d9e21af883ec3b3159716fbff75eb72ecf8ad870c406de4eb28db2", + "0x1a4f6fef19ddc5be4d70c5faa866f62997c8c4585474e20a88763ed3c2c8cc67", + "0x04b38631ba76cae653c10d4190dfb92da7cb7d1565d01eff1d7197f16202d385", + "0x29086c192de46c37d839e8c7e1f6658034f8c9d829b7511e83b0c43a305a3c5c", + "0x0d85865fb5ef1b5f5f2f197b7b09ff7ac3101478d4b2f74b6499d73900d11e54", + "0x20b5252bec07bb5be71d1dc81cf239f67b7df4fa4cb2d2b40820e70fdf7af33a", + "0x106bf0cd7dd9e9e23fb4c15417826d534bb855ec1699354405cfceaf5f413e4d", + "0x1b3349a7deee7ac0fce25b06857e5f7057ef127c174594e358b56b886d4e31c2", + "0x24e524ec0c1bf1e3b866ee4f21067df4ee4caf18a0aad0488568457e72cdda4d", + "0x2a85026693468fbf2ea1f4fc4c06876a0a2c1f523e3db4c8b42f2dffb966ea15", + "0x26d60c2e8586bdd47846b93ca706680b536913d2dd64e3d81bbcd43afb8e70f1", + "0x1513f2e297fb00c4890c20a5d8ae0e8885d26f67c60d8e11bd7a7b48f2fbd109", + "0x1e7bf38a2728e06b0285394ca652f569557a1edea9f912ed928e2a9b6558b06f", + "0x1e2679f85389940b584bd4fe02bcb473d995218aa34daede139972c9acb88f73", + "0x24cacbbe316f0c54586f9f167c7a6d8ebc3eac19a73b25a77ac12bf8608f1c13", + "0x2b394952c9c0a175c83508ca8ac5aa8d76e87afb2e404b2dc96a78cb5b01a8ac", + "0x1cf62d53c239ddff5a54c29434d738835f3be95fbdeb3a2485d0874ba5f3e224", + "0x0133337c5199bb492c3ef3dbe7947992d89f7d2347e80fcbd8b4e0b4bbe582f1", + "0x02c72c5108c7ab5f1572ea767fb980ef87a17af79e3df3617f57b93682dc42b7", + "0x209f742f8a8b3698ec6c531c25e27bfe7615f0ad831338bb45ca063ad6136526", + "0x132318d11691af75e53ad7e6c680cab1ca7f35cd2915c4744bbb02dfe18af4c4", + "0x146b29518991daa03980cf92f70113d76db9eff5445be32183438c214663d0e8", + "0x2b7f245f6c5596fcb70beae686d8084435381dd801dc0c71b6c7eb308bf13239", + "0x1a6c7bb28fb37306fd5475923c14c9a489d04ab73c4edf6625d664a76ae6450b", + "0x02f0788976dbd6590d0f4cdbcd628be8e0cc636c2f1dd45436a337f45d9bc678", + "0x2b760fa8b8b4a1b2131487bcdf25d9984067368f120f948cf13e9a80e8b56944", + "0x21480e61332508e10ca9560ad247f619019263162e172f421a797521e1b4fc88", + "0x0909867344a996e161f3323aaefc9d6ae4538512cf8c0fada1702681cd7aae2c", + "0x0b65e47adeeda408ad2aecdd8768fe1ed40fa599c31e9d105fba99e1bef04f21", + "0x2160a8038bf2639053dbd36cdc891efb15f84a017f3340456d8619f203a887b5", + "0x1c6015ff3b2171e01680d8e095f4ec599a8ab26dcf20fa417bb733900eca6ea9", + "0x2395f133ea07a6cf80caece496071970166f6503035431ff8da76cede15a3318", + "0x2ee22c9e38666ad9af08e25b24049f98f9b1dbbbc19db24b0075d9801da9d4cd", + "0x0ceafa80dcb3aeb5bc8948f3c9a44e39ffb480b683ab91e47a7d1d37a3b49477", + "0x0fd5d7123523fb0cfa789b155aa6c4109f801ec424d4ba36890adf08b5deb422", + "0x1e778393abf8edf1d93d5e4d7be8fcedf8a1675749d6973b1d977457ec9761f2", + "0x1c7fb5b032ab4e548b8b7667546476f3e2b5dc357d6464a2befd4496893b70aa", + "0x22cc4178a5d278d88827e2ad707563faf4966cad5bf458208e05e0840ef62a2c", + "0x177cf088fcb3ff3ad938398daa244fea0a1022303663034fae5108998ae54c3b", + "0x02446ae4cf3ffcac5c9ee677c231f96498e4af66f9e94d8a8a7166b0c1b1b322", + "0x2f592c120767143bbd4bb4d9b30f983b22774157836e2c23e72861598acd684b", + "0x0b06e6e8da860a94088c40bc149ba7e5c4afa20e5b87ce24b910a636a047877a", + "0x28504c08f7e47e42ada670d451d1fbdf6756dcf8afa95bbec03010853022f2af", + "0x0c89048f5a13190ff326c4a2857902c26f15e30e445038fae53723b55e2c39cd", + "0x0b94c0a4380eb16e061141f2709e5bdde5f910593db6b86733b690d4701e23ee", + "0x0910a92efdc49267b884944e35df1b962328bf417a47b68c401e954ecce3a4f0", + "0x1d495cfa1c8574575197302fdeca70997c841edadf41fcf22b94c9a0a60ecb79", + "0x016c6afa47cb0a2d059f8aeee0e893e3e903238f10d19742e4700d120d49e570", + "0x238f30dbddae035a913b74c43976fc97f02a9968e3aaea7c80bf6f586ed23a0f", + "0x20a420690ef835eb58eebd48408f1667104745e5e22317933402e843e4bf8bd0", + "0x0c4f3bcf2eea412a7919ffad3fe2d9b50e1eb0c23e85a7b0edfc52cf02cd02c8", + "0x141a8e66de659ecb03d7adc84ae3198412e000b85c7245e0b46640f6f063d6a1", + "0x1d55503e082b168071f86efad35b11c2e504b8c4e068c3893d9f49a239fbb4dc", + "0x25cb3e3034d028bc5d5e534d4dee7651f349a7bd761af4c145de51f804265a14", + "0x0a77f543b27222f91320a2d2012e841ecf14f3f1358af241ea963b760afc0226", + "0x2792d2bd839b6169f7716f7e2bc2947401861351db137948386b8aa775ae29c6", + "0x004178aae7a0dbf42f12a7912ae58a17fe2a27c70de0892eb32833d4c010acc4", + "0x2492a853a0fddc57b396601e5ab6a4a578d598ded8fa07e7fc938dca6f2dcc78", + "0x2cac8bea6916f8f8ee8f739f6f918abead13cecbd05440c6b8ee88988db1187a", + "0x05688d1db35190afb70f12b20d99ff4ccf5bfad49dfd75a355b4273e9fe2db09", + "0x2a0b02d9250b17afca1c81d9a4f9bbf0c5ed3da1b9147c4227177cebb1f780ef", + "0x176b1541d23b73318ff43d1b0cda77a85c19776f51ea48230d27b7051427014d", + "0x0d61214c2599d160e16ce1a57295f404c25fcd8c8bb8ebfba4498c502297782e", + "0x241ab2b383e6cbfeb42e2014654decda5b8d9d893b8d833129e40c5cdcf4543e", + "0x0b8b12c50e94d00a7141c6065002121bddc790de2dceb63b0a7495b73e7ad751", + "0x22991194c20c297dec8d5e1f9ed2a25f392faafbf0fe59202926f695a92a6e0b", + "0x0243a3e4d88bb5a853ff6f27bfd8fce31aaccb8fa5469b4644701645458cdcaa", + "0x27e362d929d2331b3e3d9498426c4c84be8ae699f3a80d096ddf932e08e3ccf7", + "0x11ee99ea832c3389ca71e1f8deee3e7e6b022c00f9753a11df48e0eccf7ffd16", + "0x251997eae2637370c85d3e32da766ab48232fd776061bd6526a25493d8008e15", + "0x0647a8013f249f15c2676af0978b29df1db0f5f9bf6bc48b2ccf872234460beb", + "0x0c2e31f137a720eb41551f4a7fe1631965f8122a81f81630533b66bfa02a8160", + "0x18b3638d2f111ef935f48bcc1f4c1b4ec127b6a88e1d360610d5b248a9c407fa", + "0x08d56c05e47b6499931279e787def4cf07a4ead0a22c9b49baa3f58d48ca02ce", + "0x02385a9f13f797480a2f22f476060e7c91d33e2386b231e0d85cf0a73154b4e9", + "0x011f2c3534cc8b10d0ebaaceed0fca4fe45a26fc4275586c42fbb18d137c78ee", + "0x29bc533a3ef8aea32b5b3b09cd61bada69a86f926082658e5c9e845d79e1791b", + "0x20c3b560b07d65c5d4bb7943b925791af5e4ce73ee9b85887906f7e1872dbb3e", + "0x20ed87ab51eb9f62c4d1ae45a5bba9d22e74648c3ed4ff2098f50cf154aec6f6", + "0x26cac03a8a81ba93c41678660478530c38e5a17828031e4c835265c9bd6b83e2", + "0x114d663e93a4f54329af9f168af90561ce302d063e14d9f9dda5354cd3159d02", + "0x116ff03595ed60d771cb048b9885360b23fb437e023bd79559af5bc090a22e6e", + "0x1ed2390f91c5e88f2cbae7fd350ee836b78b2b736b4c1c41e4ff62c12f970f34", + "0x0939bcde372acba7870b0c34060d990e4809391d5f3633f1f9c3097c35730c91", + "0x1d68c5b87d9e5138e18a8bd45e7bdf1046ff9f0b33f8f08e94954118705eb8c4", + "0x2e312e5764823926980664f64720f4a777b08fb7dd302b361ec7e057d19b1d7b", + "0x14c51f17e2775ec4f73e24884a0ccfac20f454813404178a854f1ee37ae222b3", + "0x25bcd5115bf23812a938ecd6f9286000e38182d590039700194f5dd4d53a0200", + "0x1988e541dd85e3b8c19bebe2c745d1e18856022b3add9a77039f68c33aeffa10", + "0x03320a1b4c2baa1ec9936c16c6fe4a482fa5282db780ef82975099fd3cd5f13b", + "0x16ebda691d0599d07ef178968a9c26eac4b07b2d8d981c3e471c4dc04d87492b", + "0x2755f04da8eb380bc527ec4850537cea4bfd4ccd8e4ef9770a8e1c6e177b6e03", + "0x1be71d46b77d343b3732d9ff3a90d08c248c802c4eba566b6050a7ab27dc5faf", + "0x0a26b20ba30f8c0d05b7bfd734cd424abdec41f4866acf1cf069d39be7eaa458", + "0x234e6b8da0038e765de5b191fedcaece417838801fcb9843d7635680d9aca5b7", + "0x09bf22199f69e4d24e1bf998ff1a81b67c047ad5fc9e57843562f6672b60fcbf", + "0x0b0f45c0f1cbc71fc33730150e8639be0e03dba9c5c7e15748bc83c94392a4fe", + "0x0b481f9325d194f000cf3105a5c1e28d27cfb35f291aedd2fbdfd04227ce5e79", + "0x11295168b1982c934326c2270d79bdd9772bad7f30b76a1b2090f1b495ac6ef1", + "0x25fa03794c7dd8bafe8c00b89409599dd73405b75a1082e69ce4c8c019aa53e4", + "0x07404b24ff53a3c80ed8b42a571394d37e2f8e07c09df9edfd3a97f8b4b0dfbb", + "0x2ea67aab8d3fa318c5d296b535904488e664315acb200beee1dccee2ddf98a00", + "0x11131f550e250562947e7a6ac53814088a377443cc71deed009d7f2b0a3ed46c", + "0x09322232ef575c60039fc86d93d1a62f74d62aeeda1961a90a9732ad99874473", + "0x22a7f10a292c3636f4f7053f1d1bc28568a990867e63e5f09c2c6a4d729e9b30", + "0x29cd0700d323fcee3861fc8cf9dbd18f4c44cc2a2ebe7ff84a3e1d9dce452027", + "0x216f0f88beda589bdc12083dbe1638c0455563d136d56cf80c8cf09156c3f649", + "0x2553bd01fc030cb010a2f84d033bd2581cb8b80138e8d9a9c4fe9f0b018bc4ec", + "0x2f58a1bafdab0a0f6904ce9880140f78de1f502c7110b1cbab7495c302040954", + "0x156f71f866df7a9bac7ea02e557b22445a8ed718ddc76aa198c5527207763624", + "0x13d2dc2b4f282edbe47e5a014acd61ce0e5da22b2f53947d56ba4ae4494f02b6", + "0x038f1198e2c9301b486334769510eaace5dcef1dfebbef3075f4baf405f643f0", + "0x0222735b9823fc2151d16e3f5565d73e84a36b4e2500b0a2ad52f6fd9dd0d2a2", + "0x07baae00c92901609b2436ed9a297ebe6e9796d8833fe28d74cb9d84209ba838", + "0x1f42fe8658e363b441d672106ba7475cc442609aed6ee61ed4324e933fd67000", + "0x2d6eaf43da341203ad9db6b728655f38e1fbfebe48904700e933e3d02e4350de", + "0x27761ecf84f77206e0a1eea13232e5b2f91559dd2943b1af70955dd674a1922c", + "0x1c680e84bc737cb665c01054c94f1dc9a4525d0cfc6fc22b43a2201a9c0bb980", + "0x03492e43461c7ef0fdbd344a6e1a9176a38e3b57304812acdfde76bbdc30cee8", + "0x1cd7e045783003e8674c551983e5c3a461556fb83ca8c161c3450babe70d65e5", + "0x1bb3693b11bfb54b846325b385b5bf5ba17dfbd90f3a7d009d3548835ffd3fd6" +}; + +// Layer 1: size 128 +static constexpr std::array LAYER_1 = { + "0x1f5dabf192d446acd07af61458d9cab348b59af3d211d436ac0a2d296ca0afd2", + "0x13af1db447f49cf60992e919d717398a6f0ddffa5ad34a46d89486b48b334eb7", + "0x1f4a9632e97496ae990cea72812afef2c38841003c9935ac569c37567a5b1a41", + "0x0802844703c620a6691842c473e2086d79888d0703184dc9b50ba04a7b201eed", + "0x2a17b392743b2e2909bdc47610fdf71d20248eabf32d6aaf53e0d814176faffe", + "0x12aafa5761c8ae5e03362f34b6467c844b96d7a15b8cb64ecd0e232bedea5437", + "0x21d7d329197a3d0adffb580a73ef44c8de3dc548ecc33c3fd27631971d118d94", + "0x23eb0e9cf0d840440af0e79bf4135d10dc5bf66327b13904724a2c43b0cedd9b", + "0x2689a28ca20d148fc8f60c40a3c3ee8cf30b51f55312331e3a863d3ce62996dd", + "0x0f4d05e9d3a443d62860e5e45c2ed729d9e2cadc3574ffb45cb7931966b2e9a2", + "0x0b67ba9f29f1856a2fc37809fdb12b6121abe45b5a1fdd0ec29c71a6061461cc", + "0x116de2be1a10c52cfec19a4950bed3fefef3c32a1106dca9b276bb565309945d", + "0x2baadb037e51025612220879d5784367f1228448777ba2248f27e908eeca0b9a", + "0x1a71125f91215ae923506b8bc2471fcfc797d82359878c93d6eac14f3492a48f", + "0x2fba3fbd538d1f6853e49cadb1e1262ec182ecfba25b9bb7137bcc9011976cc4", + "0x18d0b887e83377b44a215dd044a9d18b991748c3f6f892f06485b03ded8861d0", + "0x1bf1caabd4dbcd2493e04236d5f5a98d7f264bcf185e8d293da23cf340daaede", + "0x2f60b6005e69665f034e6457840e56a8c18bf34fb9ede2c14defc9f030909058", + "0x2127c096093cef2ae7458cd0ceb4fbfa5b97c12ff6713de3d9f04fda20dfdede", + "0x2f7cb036eac03a9de900eb230cfc1d24783d08e7a8869b8bcbaeb5aef1ec188c", + "0x2167dddfc53e5609479d2c5c18a83c009d9455e4dd35a59988e08f43ff0d0695", + "0x00fea8cb12a9932fa7db810820d40308d85056880ee1e2b14013ededb1551338", + "0x28cbbe94c206ba358d2e3e00deb478d4097b5e48d911904620402f2ca5d7e5ea", + "0x20a5eef30939dd0fa60d77a1173e2c2da574800b8501664998d6112588611d1e", + "0x1a69a0af8192c02db67533b93e7ece2e26ae7f6050e7825d516fe55506153ea8", + "0x11c98a20afe3eb1b501fb48a950cd2bf870bf00fd1135e633704d1b98b949838", + "0x0402c6674f7029a46e208a8c80d0b91c01eee57a387594b1083ecedd8c5cae83", + "0x20b367ee7c695898ea4e82bc18d4dcd9b1fb2f17c7cd5b8817d1172ba3f45fcb", + "0x27bfab05d36e42c07905c1b4a885b25f835d4a7195e71546e01096978f7a073e", + "0x02ad7e616725d959ca842dee230f335fffafb406320e31cb582ac732d6c65367", + "0x10c188f73b5e08ccbf392581c78ea6a12649b647fa6df1d1840d518b54314dfa", + "0x01e0cb0c3fc332135006caab833388c0cbc0dc47443631cb403c1a6b745a8745", + "0x23e36d0b7747a44ff04461259a00eda437797bf62f36b4aa82fbfa071448a2be", + "0x22c21b3686b60d58e30c046f69a608fc0772f8fca6c86b6d0cbf22e37e6b293b", + "0x163230eee9a601b0d592f2184fd812966357b103239b0d9e0594e60bbd4d0860", + "0x067abd7a6be9beea9a81a931bc219749cca5f45e9fbf80fa4e55155835a15beb", + "0x13f168544709d972f9234da72a74037d3fa891c8fdf555585f165852f8fe8101", + "0x2d48f3353d0f355f95b8f5521d36a449c26a1cd4030beabf06a4998104bd86d2", + "0x1d42e751a77a5695880dbe4e73d461af4e3e3fb256eea7dbbd47e335e0c68e11", + "0x037c93932be960b9bc3754f466b3fe80e402ff504587ca8943cd9cadd9a6f061", + "0x1f04f2222686a75f1ad29328c1315587f086d01cd014800361cccc20a90aa35e", + "0x0b1bd985deff689f318da1ad08b52b236a27edca0bbbbb9064a497ee5090cda7", + "0x27a021300977ee8cf78aaaf52e0cc1ed917fc68da595d8699bc8292eb6dc13e0", + "0x15824afb9f78aa47b085b6b7b31fe09bb66f70badad25eea31dc34ea4e7c63b3", + "0x0b5cdf75d22d4097f3146fa80d11678b5f57dc6d1702d7ffd90ccc9e7999b383", + "0x2827b2a9dddb4f8db0fe28707be3dbef818d84363fbe334d3401672a61b7d7ef", + "0x18951fd9b34f9826bd2be0974d00e56d3b719736ae9d58c27871fc3a1e2654f8", + "0x0f2d0c7a56a675bfa4d0703776675473a1152c9a4aae56708aae55c79a67e78f", + "0x07dc5e588bbd8eeb76ad4672afe7544b43e00c13f482370f6dd4ae6a8c975127", + "0x2dc8e0993c39e9463720e6ef31b152c8421035b0b0655a2281015c23d392e690", + "0x119bf3ac384ae7f34bb385eb4033baabb2e276336364a5087b5356db5a305e5a", + "0x250db7567b30e2d64f760d4fa0a42cb480fe8c3be470b6cdac9d506477d885f3", + "0x020f6ba4716c658ed7a44c7b6a97555ce9b7b0c32b4c0de4cdeb3c9720e3b614", + "0x039ceb640da24988d0dd57f273d5e533e25ebabb9554690d5e6f2c6b7180a877", + "0x0bdd465548752e46f2d8a08780aaa56007f403aa606fd4612a1390e9686465b0", + "0x2b0b105673e4461cc6c7f2894220458e11df6b1982e2fe2e06e19505c4ecc63b", + "0x0b467dcffb9852a55a899c63a8945c5261fe607cb5589a18e158710c3496c424", + "0x0414f89c1766a4355815ef8a3e157b519301d748127688d58c24ec5c81aed655", + "0x0575950ecc785bc29290f09f7020ebe415d278188a521bc99a00a513ec4af058", + "0x1e57ba76f92405e55a4763904ecac2b9de18e94a9a07b239d8f3048a20d66453", + "0x2b2ba1770056d35c8c94fae437dca23391fa841014589c60e9e2a35f49ea5a81", + "0x1776473271acee4df6fa1d01a582e9f29fdf32e909d4251fdbed2f13b46de231", + "0x259aa8708b0211565f243d2a1c8a5c14716c00e223f0f60d1552a54d3cdbbfbe", + "0x279bf354b9c06bfe080e129c405db9780580b257ffd3f94ee57104245be720a1", + "0x24ea06cd85f395e5d8a2d77bc820aa9ec0bfd1b132fd4a46f59569c33085da7b", + "0x1da3e49d7438a5952d5ca39734050a1fb1ad9f8da87ce5c151f9f7c77b05c8ac", + "0x0855719538d2a54639561cf7d8daf2f2e75ee1a1a15f5aa066d21dfa5405a730", + "0x2abf8ec986430646ca066c3424a5de7e31c2503bfea685f68823adb9d4b22ac0", + "0x12ac210b060fb6d966a40be6cd91b3581bfd98fb181941a057bd4aa36c9352f7", + "0x100b18e358be563a71c714c493c1587e6ef2f3c980ee1cfdcd691f417101b950", + "0x0c1b3c9e006a6b3bfeac0bba6ce607d26975c04214bba6f16aac47850aa0cec7", + "0x0c02373ad00971c34e81d2e2debbbb5ac4d3f96b84ba58975dcca7382e2b0a81", + "0x18a176d8a91424ef6f1b7877efcc04e65e8367679dccc863d4e0175f953210aa", + "0x2f448f986fcf025b0a4376435ae5d0cf7562f923910920ca0d05c3e91248c11d", + "0x17a35b326d6236c255092133b5014197e8cfe6babf07cf826bef20e0a7b701ce", + "0x08eb44eb67f8dc9128b6f8b884be1842c96841cccc749bd6100b84f742bf0421", + "0x262879a669327b98dae20ae3d9683d6cad96a4f4724c18f45fadfd628af963d5", + "0x04143e8de70cd7e844dcba12948ede3c9aaa69de48e2067a6811cc89712a5224", + "0x0f7ab3f7033d2d8fffe53b375140fda62b28d0d946e11b649a81efc113abd5c4", + "0x22f377d01948d6e624f87e83aab2b776e33572dd88b0f0dacdf4f2c73bf2f324", + "0x1cab403e7b3e3f1ec0e542282a13a5b4d7d642dd678ba310b46580bd174fdf7b", + "0x21a0d662cc1d76aca60667c5efcaf62a9fc72e9ecdd0f9f17c7247751440bfa8", + "0x202896a51167a3384cedbea8145ca32cd18a9ee9efab796d88a879d41475c81f", + "0x0a40793cd3639fc17d75ccf62ba3d51dfb968284325507f62b6353203a4a4d3b", + "0x02bd157784f19b41b43c480bf66f433617e66e6173923af9edfee5cc21dbae63", + "0x0b1b4b2926df93b6c8f96ea3c4d5e90ed3204b1d035b5c69bda18c0d7dc33fc3", + "0x2c55604e36e38fe9b9b2f71daff3217a5ef3fff7373d873aecb1948a98d03966", + "0x273a94c0a9773deb475f43a62769b061c00982ddd041864c24260fe96d84f850", + "0x1a764282cf4d87e8ed727c493dd0bce2e658857b7c8b01c5fa9a94520edbb8a6", + "0x08184661b635ebcd2565c71dd460982ceb918c7710b6f8be579e055b8644a526", + "0x19c30f733fa60e0a638baaa15a1bd4ce2a0d685c9c430f20bfbdb9ff613cd9a0", + "0x202990ffb38c063ae623e5badb30b194757b9ab05a54fdd05b42307897c8ff15", + "0x299a74d1cd13d0bcd6cc7f961662effb5bb4ee37b0d7a48c1e82474db05630fe", + "0x17aa0e6119661f2993f4c2abeb4e8e3bf54d3e4787b149ddd75d219c092eb42b", + "0x05c72d64fea57f5f2312e63331cbe66d5f9375051a3edbd25fae5759a06d8944", + "0x21ed5f80d50c6d924e26c44ce715cbd23f637bec24df2f569730ea224e23f0be", + "0x01286aab0a6b2eec0b52dbc0fd3a634df3266da68ee75320d4af49aa096b0fd1", + "0x0ab61af06caebddd37d6068f5bd992e19246a426e9e9239916fc14990b30bd0d", + "0x1ed5a6fb0eccc2bc4ddc5106e18c91787600327fd686672f198263ff44282c2c", + "0x09359badbd73fa31150156d1b95c704e3c029dd6dd6acc1e6aa1367ab6507d75", + "0x2b7910a6f7a3b1dd6f8a1f1bbf0029fe45ce29f4bde1dc384c2e5c95e920e969", + "0x20bd54fffb7769a192a1d2d4747c9cd60b67311ffe31b9acd05250a1dee5ffd3", + "0x010d92d0ac37f9c343568241172db9844c9188bf89ce44b3f39d4d31278e1ac4", + "0x134c12930cae8d8a9dd426300d427e0055a5a93895873486f72eadd86ad482d1", + "0x2c45d1c313326df1794a8dfab724ed8205352a89ed1ad50cf0a740753139e5c8", + "0x2067063e8fb649660a3881416cb86e5a75c069f42791ca5c5cabb5016f497469", + "0x144105cc8a506ba118e7d8751136004bee516d178c1e2735a0068e9de4b43455", + "0x1df67556e64db49f062d3c3ef6b1ef65b30759e5cc5b1764aa0902b57f28aa1f", + "0x2c9475ef0b9739d909a181fbd368cb86776b9954d6e90ec5384efc6ba10ea0d0", + "0x2cbe2771350fe3038af57fd1524d4b3576a65bd39e3a2468817ef6f367c5f18a", + "0x17ae965cb3dab1cf8181f67de4b62def9fed98b3b1d6a6cacd89eedf45822c3b", + "0x095642a23c15cf6f5efc484e3c1f464df2bdf6d8df46cc938740d3dae6e2daf9", + "0x2e2b138ddb465c3668202bbdeb6d03ac42395e9af1daa5dfc889dbf46be4fb64", + "0x1f3baf52822638264f585b6ec5ce2f0909ab238c61cb946d13f235b1193e0142", + "0x2d2140ddf368983ae4a503e85c9d6c33db9002d5d54b133439b1204534109666", + "0x213f6ce6752e55ec2b4325d31040541ba43b4f79c038803ca998e24a0afd226b", + "0x0719a104b633dab20a2e03d0c35335213fed9c79d28dd5b879d81fe671caad32", + "0x2f4f9b8612d313a8ee0b649472f0e166e3a6cfbb8d9b5a8dda3799d72da36f0d", + "0x26300e2e15d82506b56765a8efdee2bc52fd402be793a238f71c433bc49328ff", + "0x1311c9d2fccb6c861894dc2824d21d2c68e7169a50579d5bca76eba882aff227", + "0x114f4237bcb5a7097da6ef803853eab5d71506721e60de5314f3f7954118e66b", + "0x2fae289f8517d78b32ea919339a430b61b47da4c905a0d62e891824640dbc261", + "0x06717f4a523713fe599fb148a66945c3be7f5a1d2aa881ace669983bd63a2304", + "0x2e1470f3efb2f9fa85e5f7df28477bfcef229886cf8c187860855572ae4da247", + "0x0deb65bf890164ca628ccc1f89e96fa780f3b7f155347b68ce4c79e63aa70a08", + "0x11a7a1d4e45339e2eb27c3438d40fc7b97be178684ea818bd6ef22a459af5f55", + "0x1e73ec5658887dda6f233689750dd2f7f3b7da85a8f6cc2b796b05978fbb8f28", + "0x072556c0ed423b0655ae4d4363da90c7aa88a978ee14edb48ef6b9000e226d4c" +}; + +static constexpr std::array PAIR_DIFF_INV_1 = { + "0x0780509bfc9720ce9ee77ece08f42269a123cef0e89e83d1640bfbe166488233", + "0x2cac7e35ba458709ab3041eba9d7fa10681803b0efd7ea1a28e875b83b6fa302", + "0x1416cfa6def2632d37998afcc3d4af3aae46f1bedfcce560455a5d55984efaf6", + "0x08023aa45f1c9e262bdbecca22169962da55344e5dffddd50b784bc58661f395", + "0x0b361603a8faf9136fbd434bcd16238d77319bd9ee8f9919a6c08e5b1b87c271", + "0x1f568a731f85bb8dd1c2b50e9feff1839f8c47892d02638431a3f5466c057cb8", + "0x1aa313c358007016a4eef057e7efbc65c5712ffecbfa93507cced4dc27d5b4e1", + "0x2e8d8a3d010fb9d56d944721df27bb7aa4b9225895f6316a58e0e4a5030a148c", + "0x16e8abe222e58155af70e114ca5420ee64c0de5b9418b5c4a5c85a3a110055c3", + "0x2f638d3351e20cbd1c22b32a2b6e10761e34a308bc1632448b969a6c4c29df15", + "0x0e743369e4411409c3254fa885a54ada052951c6073aad9aceb16b51301e2bc8", + "0x054f5597bc458eca7a60518e8b35af49fd2d2d0cc59a698ce64dcb507214e115", + "0x2e4793aa27c2f6bcf580fb2b4f857cd3781e18b19a307f697dbaa88db8a251fc", + "0x0ff5c569c56b8b9bcba28afa972fe383a64605ee72acf42891febcbf4f31cb09", + "0x22d32ff4f8714d5e2853f56cc038df6d4b20090560a9a69703bfbb32de412ec7", + "0x2583104fbf48ff23291812929bc02045aa619cf9119610e99e556c23b386279a", + "0x248ab2d654f8e7b596f6609fe9be537fdcdfeca9ab8269a025e9a7cc8f83079c", + "0x2a94c3680adf4391a2a31ad62630df828a3cf71388042978b3e6e9e33e02f825", + "0x23863f191b1ddcba19ca2ee74abcc1f251db2b59bdedc8d168d56e4c1150b367", + "0x165cd02345e160361c482cb47ae38422938471121c29402886a69c124cb00554", + "0x13f2e7c444c837b0bd147f157179d6a54385bd5bd420722ad37012b26d4bcc18", + "0x27cc21134c2fd00626efa513ba56c67998849da88e8b8158055d5a840a81c83e", + "0x1482a05d98d1ee138aa7fe864386f5960735910bce6ac3ee26d439a51613ab37", + "0x000097d3cfddf6a70edcddfee71ce68c6156a4ccb99676a73135bfc6f27aaa13", + "0x2251ee3f77ba623c3ac384e99ed39e84a0e12d2bebccd7009509ae6e6b23754b", + "0x2ba06f02b3b11781f6aa279c95d841b2052698be4bf36ca9d09635db3a9a52e1", + "0x1110feadfcc9d4f0e489a8dbe473dc7caaa011ca88a8a16493de2e238de06ca1", + "0x27515d6bffb5b47162da19d33104384d6a974ab1de976d330b514ef9db18355b", + "0x07f1e21baab133edc4aa1ead81dff554f24fddce9f9531070709145539b24516", + "0x0862ec8cb7b2ecada607e38ebbfbfb29e59dd153bab858348f378c7646129072", + "0x2ba58f3b6170cd474970b09d5a9725e3e057cc0e090de4da96b0953331ad5c5e", + "0x23661599b42c925bc9a5d1f34c97df094f5e8f4b5ede392429ac208286e26b4d", + "0x04af19105ccee954720c195a17359fc8fa261e4f1f4eae5b2b8fbb3b14320747", + "0x15b51fce771005586fb4b5b695f19dc2fe97d6bb624117d46e9cb2879a4c0950", + "0x20842a8ea43bfd465c35de23c7b6dc142e3f841104a295cbbe75d9b22a6e9adc", + "0x0fb095edb9a3c139c03d57d0b7d8dd78728b2899e36cdbee6f539addf1ddf70b", + "0x1a11ba0eb97488a185df3598de6a35c3babe5173b6b3edd1f7424aa39fcccfaa", + "0x2521a518a5d48ca501759c612a30c1629b9f25d14833dab5f5611650079ffd2f", + "0x1bc0a544c65646f2ae3a47fc440323a681b046eb3f7ecaa5f53ff89ab176e207", + "0x14c8a0c74fecf849269c8c22d1b5c8d7ec86202ee017ed84514683b16ad77324", + "0x1536182d45c680a15f82a7798c394b4e15f223b9af9a8f9dea306bc30004b45f", + "0x2dc71649004dca4401312573d514ff5dec88353c9dd77770f6a3176cf97f4340", + "0x19cb444648a2a7507c06eeb0880dc26f90af75388fd84cef0c9e8c76898a330d", + "0x2e6a2e09b5483d668096c0a63a01f5f991fffa3b9a1107540fd1ff10d5c3ff3e", + "0x304bd84d2a8ed3717fa128aced2e7ecfcc26ec69cef72bbf6723e40e176ae5e0", + "0x20811956a9b28f68e6352afd918b2912468eb6324438ae2d89fa8687aa754d79", + "0x28a896d0eda7b9fb42a35a0c3194447c10e1c71b8ff3374e6f6b134c33522d4e", + "0x13351eab1ef092c0ed485afcb941da8dc27956fd279482050f7063edb705211c", + "0x1301768cccbd521021f481d3f2dbf514ccf7f52d8004c143d1a28e4068b2e944", + "0x1ae0c8427470ef5985c5e0b4010ae84bb8270a3a796f01d5d0679a2f99a97613", + "0x179364fd4de1f5d73fb68f44bc2a9bd87d6bfedfe9e64ebeea87e3d23c78521c", + "0x07c5b8cc9fbc2b3edad3299601bef81ae9b78f21e2e4add6345ae6f1c8ce129b", + "0x0f4dd7aa105be71c76a6f8cbcb0c74dc9e83d2ec0e95f54f4b6a8d5cda009356", + "0x0b134dd8af1e3fbfac6cbe5487d57d7bdf610787ca9ba3487fefc5eb15a7a99f", + "0x0a57d0fed7d8b421c5a502a324f70cd5b48e0ef3ee4129a4c5278a36b32f6c37", + "0x18af679c2eaaf30466cba0136231985b0fd930ae7897400439ddc4477e850338", + "0x133be802ef7eb7e25f68fd3f4e8cd30dec65cda2a745645c9014b48a02e14846", + "0x1f162142b240b6a26f0176b36966fa928f798d7661b9d59500098a28d639e649", + "0x18772b8322e455ef467442d407ad15f56fccdb4e6413cb00161a74e33bf6836f", + "0x20aa7c81eae6b653ca5b13d06c9b2a7d817d565362074ab8082f5bf9899227a6", + "0x1ddad686bf254de63f9151faf29d0f79d388d5522e99b3e0d0ae698be0c87001", + "0x1339866a5a99d732b7c79fe8015f18e2dd02521d1e71abb557a8b5578dfbe343", + "0x122a768152ef527b0cc739e643ba0c919db0d2a7acdc42d25d02e02204bb89d2", + "0x2a81ce6448e29b5b6a9d0c45371fe7e1a64f4c20dcfee3670a83ccdbfa8ea9eb" +}; + +// Layer 2: size 64 +static constexpr std::array LAYER_2 = { + "0x194796f5c8f5da2d12d05094df7f4d94453c971f379513f8d8cf39122a85dda8", + "0x0652e6886c5b4025a0a20fb5c9a11bec5c82aa0235d6258361be20a193986abe", + "0x2d043a71b3a6d9b8f465d0259a0c22857e2fb7ad78f0505530be838d343d1213", + "0x07c1f7473a3724879cd131fd570cbf2de71207bd3444c93b745ef029dd319d08", + "0x11c3b8d42a78e29cda1453619d1482b777e952213dcca1cae2cdc4dd11625650", + "0x281a45e44be6a25c97000cb48a0e05a28dd26076777293550dc770a9c4c85e29", + "0x02f2f3fdca12a5e14859e6c99f5a24dd837ab0053404d8ac74521b41b511afb6", + "0x04ed2a0e710fafa1c3253d839153f0addcf71a48def18717074675a16c593b77", + "0x142afd9bfb4f3719a1c407bd5214cbb58d55e3d72364f0fd4695f6c208bafae2", + "0x139179b8f3a143cb9c56df2c7599803b8b0cee79f90415f9a0ecf928065afe1a", + "0x286f487b28b359f0a6cf61f8f2b89d98ddc46021b41f6c99a1dbc0c313a7b43c", + "0x1fbd5a5313693f82497b5bbd15831ce19ba49a027873388835d26e89fba4e920", + "0x26d338e097b17b8956b696626d655916da8053b71c4db094260588910722c2ca", + "0x23e98397098dd0958a2fee5996dc2eac358ad70d3d615316b24cbc150b994755", + "0x1434d7eb06f84a92bd7c5ae9c1a6fc172872e84f1bc2ac96e52d5e76b2a295e3", + "0x10c4148eb1aa4c34d8cc5f58ade16144b813e61bb22f794669aa452ab6daa84f", + "0x0d9cef21004809ddbe780763be8e278492c3b926b27025b529375fd5e589e1b4", + "0x26017099dab4daa613074f22325e25159d1a4c68ba44d22e0191b38ad230a35b", + "0x16503b71cad28ffd9de5ce7da196776968e98a9418a2accc99c86bd3c2b4fa58", + "0x0ebd0daa6e51d7f9d0293b1df724ca84af9ab5e60d6198fd2e41aaf4b995b922", + "0x29892600db8f8f0f1ddc3d234f1dafd688c35951ebbfa09bea2fa34c86c5059a", + "0x117e269dcae8c4aa92d7b86725b01cb77eb936b0ad34ff237105a83794f4a39d", + "0x2a210319a91847b9b093b8234d2c7290a43688ba42d50cfc442165dccc0772ab", + "0x1ce067ea62df1895571f3e4bfd2cb4d1a1452d6387c8e210f42bc334834568c9", + "0x09dfc769010e45b10d9a33073ad4635348ce2f55fff8799e833a1bcca2504aa9", + "0x1f46032bf77974ac97884463a9739b8c45e611927cc2172a01f3055177b58e00", + "0x232a08842075d572f3aefde91af2be89ff44e2e26fb063da3b4cb7195375d8c5", + "0x15dcdd24e0235c6e3a24eb7bb28a66b0633df44254a84ed3aa42e9c9c91cb23b", + "0x265a040e50b01117b984c44f7d6d7a9d1ad963237944af4e35c2800acd2f8b97", + "0x1fbbbf6c11eb9647807bb9554e63f23bc845875954b73bb1a2d8170b45d15834", + "0x1bece905cb6325f0044ed4703960bdae5925c058afa48dac570bd7215a7b27e0", + "0x29325d36a62f3d69c03057b3aa4f8532de6ced3f040d212a4abd32ca285ac8a5", + "0x2a700a60131271001d9a05a1d7418191fde87ea85915c7d3cafb71ed83900331", + "0x02781a5da392c8d084948e038404741fd580c79dc33784815aead9a216fb39a3", + "0x0a07bc20a8a0c2078d21c623efe97c51151f0dfd2ca76a485646ec308ed487e7", + "0x15148bd1babd56dfd185c8beb5843837dbf1274118220d212c467a0f51ce2a02", + "0x146a5d31eedb88ead25fefc7a7f905bdc13de637ee5d270be274570e6f7ebdc5", + "0x23062c6be8b49c9b920d4b2b503819620998786e33c399e70e268c487102da00", + "0x23b4accbe511ee1ced67094acb084bd36e185d7d7bb4ac9824355ea36e30f977", + "0x1c2cd8cfc9f78c087c0e43dfb3fcad210cf13d947606bf18ae4c78c2aa57c3d4", + "0x204aa81be9e712eafdcfa42836db1b4c31832520efb54a8b89a3aebb67a3dc81", + "0x0082c3fb1ee3af9fa578a5f333f271c01baf823865d37b67f87fef154d39956b", + "0x10e10b3343f657c87a25066efdc79a7bbb985e1f6439f51a72fe59f228ef9b90", + "0x0878a48935f45c81206575fb6856a843a53df51ad9b36bca1314d9c55b04612d", + "0x0cf1399b8df2780b666874a89eff0b54128aa03c2071dc40488b6b2fa807a7ae", + "0x29e5be51c319302ba5a62b468cb5ff6733fb0a84107e4d30ecb0004356dd1cd4", + "0x05439a6d17584790a8605a19f03beb9f17265a6492f9f5087d2b8d3ef107d48e", + "0x1de781c6241be2f325cf8140f28ccb61671bb87ec4ece30c853f57dee727132a", + "0x0b07561d1731e8bc487ff53559d93039c1e0952918e2d26a6d8e2c8485db9fe6", + "0x220474226e8e1f06f02bc562b6045a13878283b744b6e40acc2333fa7a303b2d", + "0x13bd18c0dbe17dc89a0b0cd85b55ff21ca39a3836b35adb7ec3419461ba0481b", + "0x1b4d0873a08d365ce46bb6276f6959126101062fd72f2c858d65d4d41034fbb9", + "0x0e8d3f52b8ffde0503d519076df0bb1dfcede24898d1a3a5bb138ab9f88ab3e8", + "0x07ec6b20d0a35acc289b3f8ba54b9edd01ccb4f15575b9166fd668682c836adf", + "0x070d38ba0e7b50e811f289352f0e605e96b86e507a896c15585f764aba56e20a", + "0x131cbe6020ddb03d490f51b625773afcb68dac2e05c09105088822d3d4fc0bbd", + "0x21f9f2b149ad9772fa33549f20ee77a80c5bfbfa6eb13874699c96dddb8bfb31", + "0x08c305724cac795af4b30422363e8449ea10dc0ed5568bb3abe610c84fe9ec11", + "0x114b4702b00f0d850e336aa356906247a79a67414ff25d7ef3ba6b8c286163fe", + "0x216c0fa19904fd7a49dfde74359716f90902ac4b9c19c02d70a7fc225c8359f5", + "0x0e16eb6d398635c158d44a08804aea1d4eb5667b9c130d44ef5ebf6b11f0b7e4", + "0x2e821bb0e75fc5f50424a90072ca170e0ae5df7b29b666b4262c7ff473f99228", + "0x190e78fd93b88ccb37f9f6b8501d074ea0eb05e1ff6db7b3c5ed4d0a59f6a241", + "0x03c12e4c5730a49ec76ee2e462bd2281ebd0864b206edc7eab975f49f768e148" +}; + +static constexpr std::array PAIR_DIFF_INV_2 = { + "0x2a3e9466f3ddf3161b38d1bf8ea022f922012efff1965390bebb59ddbba33c45", + "0x2596f1c361e61e5f6d378a258c21f46efb56a24751b92d632d073b8249eb5416", + "0x2991d8ca02f207dfba11dfa5ddaa44c9db56159c099609982b73c15516835d64", + "0x2d3e72a30ae2eda76df5354f3388ea9a12c396c618bc98e50d06ea80bd0a14cf", + "0x2d3834f4a4067037d8ecee1595b015bfe16be37d38039aff09dbba7a34af4a55", + "0x195d77c3eae95ba6cb430c5f6954473dcd3f26b33196050b9bfaaee4eab803ed", + "0x24d077bf7df9575c9fe62db66046a53d0657b0599691578a6b8b2462ae5fc8b8", + "0x174d5646b341bd4c32ddc7a6db95fd4ca1f639d235d20802b33d42d17e83df4e", + "0x08e9dbb478e672795196c9e171b2a6b29e3bbd2b1cd294bebf65f912a090bdb7", + "0x168999db3353b93469be48aa4f8b6b1b10ab338fd594b499a64910ab16d166eb", + "0x28d6a5ce3636e43b8633ecd341c14610c4fad018d6e933a4d15415befc471b44", + "0x19e4b898ae6d912ab66158e0a772987d1589f1f131e165b0f4fd7c0bdc8f95a1", + "0x25078801d2ba95221c328c72b4d73c0bf888118531f98dcb3ee95a646524440c", + "0x12e567672ca28b0508056f6aa63251a4893034bcfca1e293837b0271262a73fb", + "0x02e1ea1b5ad8416c2c2d0718f4ad4dffc864916bd4fc1708fcd5fa7a27907850", + "0x28c46b0eae0a01a4211d7544b7323c5fad25f4a684e1e471a6680af15907fae3", + "0x2f10cffee541c35968d2e695e15c8be9dd4a0d9eb0c04038cf5ee1ab10386512", + "0x0e4a4a489bd73a9197613acaa36b6af66a299aa2b894c039aa1f01bcad86f246", + "0x1e2acf678ad9e37ada0f3b11bb7a1ba6cb03c6a9a93f98c20c5586e17f6f9035", + "0x251e5c6b1e4d12321a87f4cca13bf14fb738305fef9702196b5629243b3d9d1a", + "0x22de306140ee518e947015bac1d7d4ca191eb79b6b18dac46dd21540fa5a6867", + "0x2e986dfc59ace07d6bcfa52b2a639aab7f766980456c3864dede75b76c804fc5", + "0x1a3486a1e5c4dab6fa79e2b32843fc711f4b9e769ed692f0ba3a4070ea1167f8", + "0x10c0e872a926112639e4f0771d5ddaaf9e0bf6f73dc4b9c25c1df3a67b7b8a6f", + "0x09baf1fbd768a4da9408d23090471642fb5e0a1d74b09c932df8a6a3084e26cb", + "0x00045a9aa543868a5a422c2d0856707de1b336cf186df2c2a1c024f58b2dafa2", + "0x27ff642fe8bc69c2aaeb410f0286414acc63c8e0a7fec79db4bcff0ddafd1fe0", + "0x288e27fea0bc5ea107445104b5b4860d51c71d238818e8d321d55b7838761ef6", + "0x083859cbbbd8ee1dd3334f67c512968cf5eeafcb508eb421fdd627bba65c23ac", + "0x0ac9ccb1e88a72815d93f2b496a83e56b5bfc4ad1903100b3d2c3c34db201ca8", + "0x299471ef252bbe8986944cdb430d0346ff80484896fa508ffbb9e141932fb894", + "0x1fb87b220564f82f0d98c87be7978919c5aaf60a65adc3640c43614f92705b5a" +}; + +// Layer 3: size 32 +static constexpr std::array LAYER_3 = { + "0x226aa3b9b245a28796ae405c7c8c08b8707c1fd5973418d220a005fba2058998", + "0x17e251bcc75d007a43cacd9594f221fbf6dbe63f6808b197759ee15677004a67", + "0x15bef8f63284f31ae7cb806f4fc0d868c0a5cfb8ac20f7a303dacab9b70142b9", + "0x2bedd3efac6372eb8ceb2a9853dd895587dba39dbb61ddef599b514bfb6c6d10", + "0x04e11869ef91c2e212b82d4f0ad8c207667e426732b305dc421776e774d0bcd4", + "0x29d374b40ad896528f514205a0115896c4c1e2f2b1bf6a4198c357ee29bae0e8", + "0x055aa32d8561eb589c04da3a302daa431eea1790b642c24a155cd4e1173251ec", + "0x303153b4f276332e5dc7b13f8c9d2fbeaec0cc7cc3f34dc26e88d576e31da551", + "0x1328a81bbb73a15f05d7960b4ebb2093ec30130619a3788e4d0f0079644e8022", + "0x232b8e8ac9f3eaef6063b4fbf0d883eb6b94e551cdd298f45262cf502001398b", + "0x1803561242e709138738528db64b71a6c6b3c84f1ee29eb991af75b13086f88b", + "0x06e901401f9af35dd024bbde43a4feb76e39992b58afe157c5bca34b4a98f30c", + "0x127774dffbe14aef2362f530d22f9dfd1a61fe014348c9d9eb664ebca31a1337", + "0x2c82444ca2e4581b961a03c5e95d67a596dceb9f5468dd4d1bd2175456660ce8", + "0x288fc32ed5bf89a78470e4dff92f79a60471b7531db7a9321b4ed2c870171077", + "0x0d5e98b8ac03868264dfcabf663966384c86a8a87da599586bbef80591f16438", + "0x27bb9614cee8ea1e258c2c755fb3e9ae197cc2ef3a4dffb24fbb736d37d227a0", + "0x26b8e7201f8051076976feaaae2db8bb51f3da2e0584f33e4a8a428140508747", + "0x08c056967cf165209e34c57bc2b7b01d607a38258a61978a02d1e015d244eb32", + "0x08bd1881e51c65b11ad8db6b2c595d293df2c623eb1a0288387cdac4bdba5d9a", + "0x16c967b76accc46e87f5405082d9a486b30845a88b1a814722188902733f6241", + "0x2881e29552fb16fada0727cf12484d84455e604171a5bfcc99d1f7b28de4b482", + "0x0fe13e378dd0effc28ca2b7e42060c8168460118c3e7b617195637237a4dfd74", + "0x0eb028ae59fa202d06727a27e86f29608529e39f9412b01b798941044c311d45", + "0x0a8cbc7e20f9347e6e1171cc218e148d8281355e7532ef1869ac0da671cbee99", + "0x06bc0b021a634561f27f32aba57d59685d4df7af58a1dfe32aae7115bb8f22d0", + "0x132851eaa6c23a52682652b2374e5a63d4365431c62bfe5eabdc7da16fc6e582", + "0x15fbef2a4f65b142ea48b415adecb73b9997aa9bf74b4c0697c040e8198fb4ef", + "0x1323f1df60739e33789cf87dc3839e4c96e5d3ad1be0f998a1f69a71d30fec3a", + "0x2cf0dd80cf88b396eae44c7b86f942dc008270e284f6df6b45d9f1fbadba931b", + "0x13ae646735590a15a28cb54e4f48fe8f2767d048b59b826599ce7f27a86172e0", + "0x0ba68de6d39d3962ede324bdd2d7e146f7947d982b053aae7329ed1013b352ac" +}; + +static constexpr std::array PAIR_DIFF_INV_3 = { + "0x15a002f4204607c71c4ad63bc61f95b82104bd2b5574c7cd997e4d9af4143fc6", + "0x290f787b68203d542b2395e42a477cf319b40d1c3e3d4ce2a7af4d7c2c872dc0", + "0x0954d2b1322249fd325f941beca783873dbada2177820b4d9b76684abc9cbdbc", + "0x077fc7eb6f421eb06692a93230d5a4e24320812c6bdd2d062f2417b6879e6a1c", + "0x2aab780f0c33d39e155e00a6f53009c55d3ad9027e1b6ca4371fd2d13783bdd7", + "0x1d0551747b3f8dac998d69836c4998a4fd3e3ff26ec8de4fe7bfbc1ecb3b6161", + "0x2a482706434b4dc5294622abe0666bdd5742aa0436c72f2646df8151ca5ba62e", + "0x1446754b4086b569aa2a10c0478150737657d78038a6f47d09a6ff4978110034", + "0x21fb56d38efecc84d831937646705996cb5d2a62a79604790ea0058621527859", + "0x0ff7065650c60b422f13ea23f70bab1d63a6b3c9474fdffb11d47c1fade42399", + "0x05a8b72ec5b7683b61508023b565d9ccedaf9eac4f8972e96965ebda39dc87b7", + "0x2f555d4b583e097ea1c5da98734f612bbafedf3d5131fd9eeabeed4dfb1f8223", + "0x1aa6023524d1a9eb1470f475163f5e1b193ef092977739296a0540b4652e3456", + "0x1fa04f8dbf5c31552273764fd1cabb4f598e5c0e4fe4d147316aaf09568e3b67", + "0x20ef605295bef49277680bcb8bd5a4ffdd20b42b488b8c5361c501bad8b650c9", + "0x2196e170c531cd3234e48b7480174347365e8afe911f661289e713791f82c525" +}; + +// Layer 4: size 16 +static constexpr std::array LAYER_4 = { + "0x0f17c47ddcb0ca8da67365bb5a813ce9c2ebbb6d6bc1364a607b5969d26b6710", + "0x038cc38c425f8f69977ac529c161253a81c2991607ccc29bb04903d8afe48786", + "0x13d528aeec2a364d2889848b123b2b66f1944b183933778032ed16e75a56e10a", + "0x2a00c593ce33b6ae4a4d44a37ff9895f9642acfba92cc8cabe5898288a377dc9", + "0x11005943971265623d36ac3f8d75096ee9facb49c07e6f7690706c01b920d234", + "0x1746e1f8b955eb35539162be309af09e43131bdcbda447d422b52fa1b0334b42", + "0x0a91ba874fe6b9666758445871f659a557a45be37cdb60b45af3781c6291027f", + "0x03d30712a7f291434e732450f34da3a26cdd88c4f2451ba3d831f67c27e2786e", + "0x130b3dbc1920b3ef16724677700bd8023f258b9e91874ff9e2fb7a37a72b21da", + "0x1f3d72af210b0e62f56c2647961880349957203b2925612aa951ac7daca10f7a", + "0x2081811f265d217791e7e3dfed5c6eeb6b5e5fbae7bf856b69cc5f6a715e912c", + "0x123ac98cabb482b25cf6ae93f15458d3d845870152ac15b189bd504b35395b1a", + "0x1af13fe19908c7343e892c4e9575df2a81bc14e861daabc5b99d5546473ab290", + "0x1e64ac7ccdef499a6b37492aee97f504d052352a739eda7e51cbe950fcb455db", + "0x012fb245669ad1a511369317c6b9c2b864cc60446d92495da53d31f1110c392f", + "0x0e5affc1bc549df6f54c2e1d38d3ea60148f697aab5bbc5a0b29512d76b56a03" +}; + +static constexpr std::array PAIR_DIFF_INV_4 = { + "0x1bcd3fd310ac8f7fa026c8cec6515ddcbefd7d1ffe95b032436c624f74b988cd", + "0x0e05c749c7bd83d91b0ae87dfd7df49a55d92e1452b248407b4f26b77663c34a", + "0x0c982ae7a3acf6b67f1e065ce154a1cdff3a94f5912c875c071f6e67c2597f8a", + "0x2b7464913fc831c1d43501de3a2635540cc7da2286d3b344d135b592a2b30362", + "0x1cdc0e8315cb2823ea8ea88ca0b0030239727fa261aa42aa5c62473105f2a008", + "0x13a30057e14f803b4b680ff25bd303a7580a065675f1cb0c60d970aa1212aec5", + "0x1a126db6ae0dae75a96998c25dbd371fdae800895ec731427d2d16bfc3bb4319", + "0x0aecc9952467256064e7d5b02ebb60670851907f48e080d38a87d1b90e9314d3" +}; + +// Layer 5: size 8 +static constexpr std::array LAYER_5 = { + "0x2df7ad4ac2b94725be5560486bdaa4b99575f6e0246e6bd5f09e354ddb3b83c0", + "0x2e9ee14c305266758e569f86f8c7353cae7e69255818095806c21202be2a91d6", + "0x0fc7066bfe3d80440390d6ca1f63d1c258d5f015dfa717f00dc04be754dd6fc5", + "0x17abebbe659e61dff0636196911a19a36a6b793fba8cf980b91cbe094898d672", + "0x0761f5c31bd15515c4df46ed42b72009679a2574e10d3640bb1496dd89838253", + "0x111beb1372fb5d4f07e81a483eff1d130f489649eff73d56e587ee88360f9eac", + "0x179617dd836953b479fe8b85d9fdac2b4fd56bfc11938fa3ad580bb9d5423684", + "0x1e02b1e5312ef7e3452f0683cd6f1dd014d1a213c4c6bd8f9082a956003cdd47" +}; + +static constexpr std::array PAIR_DIFF_INV_5 = { + "0x06c858cf72446fce70612c6103e3c25ad06039c28bea83a266849b3d0845d853", + "0x1984bde10405afb3cc7a6d1b70eb9628235680a82252b2c8b0c3482929a6548b", + "0x10c3a3eb7c7bc5630d5d520291851f31c66a0a80c7ecb63120d01c190ff2fa16", + "0x08ae33db7320f593215d4cceb2532ee7f93bb10d15297b4c991e298e70a3922f" +}; + +// Layer 6: size 4 +static constexpr std::array LAYER_6 = { + "0x06b72f0538c90947aa4a3a158be29515daaa6ed5da3d19fe54ab31c4ba45a6e4", + "0x11185856fd8c30d0bd544caf151722a29b6151f01cd0be969542662449c0d153", + "0x291ef8b3bd16e12e5cf53ae65833a69e1dc719242e6dea08a031495158234461", + "0x070c299af10bc6cf5ca7fafa3bda07c65cd76dd454152ef7f297ccf89e5c548a" +}; + +static constexpr std::array PAIR_DIFF_INV_6 = { + "0x27cda6abfeef4c10507a1f83db766aabfaa09102d0a7835f9526137f55f957f5", + "0x2315146546522db8741f3c494f5594d10f158f71ed20d39b9d2a7ebdf33c654e" +}; + +// Layer 7: size 2 +static constexpr std::array LAYER_7 = { + "0x162bda1ccb9a43f388bb4ae9ecc88b18be5528f83e18e3292ebac419eac9b7e9", + "0x2ede82c8a583f14753c8634ddb24d22b559dcb540ec5973dfdd9083798faefc8" +}; + +static constexpr std::array PAIR_DIFF_INV_7 = { + "0x089c742f5cea243e6a3a079f460707859136290fe8d797369be10e67b2b5e2b3" +}; + +// Layer 8: size 1 +static constexpr std::array LAYER_8 = { + "0x2ded563fb0eecdfbb722aec6a8dc5ff0afefb30d6287aa55d1a0f165ae902aa2" +}; + +} // namespace bb::basefold::domain_data diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_precompute.py b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_precompute.py new file mode 100644 index 000000000000..995041aaec82 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_precompute.py @@ -0,0 +1,904 @@ +#!/usr/bin/env python3 +""" +ECFFT Domain Precomputation for BaseFold +========================================= + +This script builds the "ECFFT domain" — a chain of evaluation domains connected by +2-to-1 rational maps — and exports the precomputed data that the C++ BaseFold verifier +needs. + +Background: why ECFFT? +----------------------- + +Standard FRI needs a field with large 2-adic roots of unity so it can build domains +{ω^i} and fold by pairing ω^i ↔ −ω^i. BN254's base field Fq (= Grumpkin's scalar +field) has 2-adicity only 1: there is no ω with ω^{2^k} = 1 for useful k. + +ECFFT (Ben-Sasson, Carmon, Kopparty, Levit 2022, "ECFFT Part II", Appendix B.2) +replaces multiplicative-group structure with *isogeny* structure on elliptic curves. +A "good isogeny" ψ: E → E' is a 2-to-1 rational map on x-coordinates, so if we +start with a set L₀ of 2^k x-coordinates on E, we get a shrinking chain: + + L₀ →ψ₀→ L₁ →ψ₁→ L₂ → ⋯ → L_k (|Lᵢ| = 2^{k−i}) + +Each ψᵢ maps exactly two points in Lᵢ to one point in Lᵢ₊₁. This gives us the +binary-tree pairing structure that FRI needs. + +The FRI fold formula +-------------------- + +For the "good" isogeny family used here, the denominator function is v(x) = x. +Given round i with domain Lᵢ, degree bound d, and verifier challenge z: + + For each pair (s₀, s₁) = (Lᵢ[j], Lᵢ[j + m/2]): + + e = d/2 − 1 (degree-aware normalization exponent) + a = f(s₀) / s₀ᵉ (normalize by v(s₀)ᵉ) + b = f(s₁) / s₁ᵉ + fold_z(f)(ψ(s₀)) = a + (b − a)/(s₁ − s₀) · (z − s₀) + +This is *pointwise*: each output depends on exactly 2 inputs, so the verifier can +check a single query in O(1) rather than running a full ECFFT decomposition. + +Pairing convention +------------------ + +Points are paired by *first-half / second-half*, NOT even/odd: + + pair j = (Lᵢ[j], Lᵢ[j + m/2]) for j ∈ {0, …, m/2 − 1} + +This is set up during domain construction and verified with assertions. + +Usage +----- + + # Generate a C++ header with domain data for log₂(n) = 8: + python3 ecfft_precompute.py --log-n 8 --output-hpp domain_data.hpp + + # Generate test vectors to validate the C++ implementation: + python3 ecfft_precompute.py --log-n 8 --output-test test_vectors.json +""" + +import sys +import os +import argparse +import json + +# ============================================================================ +# Field arithmetic over BN254's base field Fq +# ============================================================================ +# +# Fq = GF(21888242871839275222246405745257275088696311157297823662689037894645226208583) +# +# This is the base field of BN254, which is also the scalar field of the Grumpkin +# curve. All domain points live in this field. + +q = 21888242871839275222246405745257275088696311157297823662689037894645226208583 + +def fadd(a, b): return (a + b) % q +def fsub(a, b): return (a - b) % q +def fmul(a, b): return (a * b) % q +def fneg(a): return (-a) % q +def finv(a): return pow(a, q - 2, q) # Fermat's little theorem +def fpow(a, n): return pow(a, n, q) +def fdiv(a, b): return fmul(a, finv(b)) + + +def batch_inv(vals): + """Invert a list of field elements using Montgomery's trick. + + Instead of n independent inversions (each requiring an exponentiation), + this computes all n inverses with a single inversion plus 3(n−1) + multiplications by building a prefix-product tree: + + prefix[i] = vals[0] · vals[1] · ⋯ · vals[i] + + Then invert only prefix[n−1], and unwind to recover each inverse. + """ + n = len(vals) + if n == 0: + return [] + prefix = [0] * n + prefix[0] = vals[0] + for i in range(1, n): + prefix[i] = fmul(prefix[i - 1], vals[i]) + inv_all = finv(prefix[-1]) # the single expensive inversion + result = [0] * n + for i in range(n - 1, 0, -1): + result[i] = fmul(inv_all, prefix[i - 1]) + inv_all = fmul(inv_all, vals[i]) + result[0] = inv_all + return result + + +def poly_eval(coeffs, x): + """Evaluate polynomial c₀ + c₁·x + c₂·x² + ⋯ at x (Horner would be faster + but this is only used for small test polynomials, so clarity wins).""" + result = 0 + xpow = 1 + for c in coeffs: + result = fadd(result, fmul(c, xpow)) + xpow = fmul(xpow, x) + return result + + +# ============================================================================ +# Elliptic curve arithmetic (Good curves: y² = x³ + ax² + Bx) +# ============================================================================ +# +# A "good curve" (in the ECFFT sense) is one of the form y² = x³ + ax² + Bx. +# The key property is that the map ψ(x) = (x − √B)² / x is a 2-to-1 rational +# map on x-coordinates that sends points on this curve to points on another +# good curve — an *isogeny*. Iterating this gives the chain of domains. +# +# We store B (called `bb`) and its square root b = √B. The square root exists +# because we only use curves where B is a quadratic residue mod q. + +class GoodCurve: + """A 'good curve' y² = x³ + ax² + Bx over Fq.""" + + def __init__(self, a, bb): + self.a = a % q # coefficient of x² + self.bb = bb % q # coefficient of x (called B in the literature) + self.b = fpow(bb, (q + 1) // 4) # √B (works because q ≡ 3 mod 4) + assert fmul(self.b, self.b) == self.bb, "B is not a quadratic residue" + + +class Point: + """A point on a GoodCurve, in affine coordinates (or the point at infinity).""" + + def __init__(self, x, y, curve, inf=False): + self.x = x % q + self.y = y % q + self.curve = curve + self.inf = inf + + @staticmethod + def infinity(curve=None): + return Point(0, 0, curve, inf=True) + + def __eq__(self, other): + if self.inf and other.inf: + return True + if self.inf or other.inf: + return False + return self.x == other.x and self.y == other.y + + def __neg__(self): + if self.inf: + return self + return Point(self.x, fneg(self.y), self.curve) + + def double(self): + """Point doubling on y² = x³ + ax² + Bx. + + The tangent slope is λ = (3x² + 2ax + B) / (2y), then: + x₃ = λ² − a − 2x + y₃ = λ(x − x₃) − y + """ + if self.inf or self.y == 0: + return Point.infinity(self.curve) + a = self.curve.a + num = fadd(fadd(fmul(3, fmul(self.x, self.x)), + fmul(2, fmul(a, self.x))), + self.curve.bb) + den = fmul(2, self.y) + lam = fdiv(num, den) + x3 = fsub(fsub(fmul(lam, lam), a), fmul(2, self.x)) + y3 = fsub(fmul(lam, fsub(self.x, x3)), self.y) + return Point(x3, y3, self.curve) + + def __add__(self, other): + """Point addition (handles all cases: infinity, doubling, general).""" + if self.inf: + return other + if other.inf: + return self + if self.x == other.x: + if self.y == other.y: + return self.double() + return Point.infinity(self.curve) # P + (−P) = O + lam = fdiv(fsub(other.y, self.y), fsub(other.x, self.x)) + x3 = fsub(fsub(fmul(lam, lam), self.curve.a), fadd(self.x, other.x)) + y3 = fsub(fmul(lam, fsub(self.x, x3)), self.y) + return Point(x3, y3, self.curve) + + def scalar_mul(self, n): + """Double-and-add scalar multiplication. The group order is q+1 for + these curves (they are supersingular-like with #E(Fq) dividing q+1).""" + n = n % (q + 1) + result = Point.infinity(self.curve) + base = self + while n > 0: + if n & 1: + result = result + base + base = base.double() + n >>= 1 + return result + + +# ============================================================================ +# Good isogenies +# ============================================================================ +# +# For a good curve y² = x³ + ax² + Bx with b = √B, the "good isogeny" is: +# +# ψ(x) = (x − b)² / x (rational map on x-coordinates, 2-to-1) +# h(x) = (x² − B) / x² (y-coordinate adjustment: y ↦ y · h(x)) +# +# The codomain is another good curve with parameters: +# a' = a + 6b +# B' = 4ab + 8b² +# +# The denominator function for this isogeny is v(x) = x. This is the function +# that appears in the FRI fold normalization: we divide by v(s)^e = s^e. + +class RationalMap: + """A rational function p(x)/q(x) represented by coefficient lists.""" + + def __init__(self, num, den): + self.num = [c % q for c in num] + self.den = [c % q for c in den] + + def __call__(self, x): + x = x % q + n = poly_eval(self.num, x) + d = poly_eval(self.den, x) + if d == 0: + return None + return fdiv(n, d) + + +def good_isogeny(curve): + """Compute the good isogeny for `curve` and return (ψ, h, codomain). + + ψ(x) = (x − b)² / x = (B − 2bx + x²) / x + h(x) = (x² − B) / x² + """ + a, b, bb = curve.a, curve.b, curve.bb + psi = RationalMap([bb, fneg(fmul(2, b)), 1], # numerator: B − 2bx + x² + [0, 1]) # denominator: x + h = RationalMap([fneg(bb), 0, 1], # numerator: −B + x² + [0, 0, 1]) # denominator: x² + codomain = GoodCurve(fadd(a, fmul(6, b)), + fadd(fmul(4, fmul(a, b)), fmul(8, fmul(b, b)))) + return psi, h, codomain + + +def apply_isogeny(psi, h, codomain, point): + """Push a point through an isogeny: (x, y) ↦ (ψ(x), y · h(x)).""" + if point.inf: + return Point.infinity(codomain) + new_x = psi(point.x) + if new_x is None: + return Point.infinity(codomain) + new_y = fmul(point.y, h(point.x)) + return Point(new_x, new_y, codomain) + + +def build_isogeny_chain(gen, k): + """Build a chain of k good isogenies starting from `gen`, a point of order 2^k. + + Returns (psis, curves, hs) where: + - psis[i] is the rational map ψᵢ: Lᵢ → Lᵢ₊₁ + - curves[i] is the curve for layer i (length k+1) + - hs[i] is the y-adjustment map for isogeny i + """ + psis, hs, curves = [], [], [gen.curve] + g = gen + for _ in range(k): + r, h, cod = good_isogeny(g.curve) + psis.append(r) + hs.append(h) + curves.append(cod) + g = apply_isogeny(r, h, cod, g) + return psis, curves, hs + + +# ============================================================================ +# ECFFT Domain — the layered evaluation domain +# ============================================================================ +# +# An EcfftDomain holds k+1 layers L₀, L₁, …, L_k where: +# - L₀ has n = 2^k x-coordinates (the initial evaluation domain) +# - Lᵢ₊₁ = ψᵢ(Lᵢ) has |Lᵢ|/2 points +# - Pairing: (Lᵢ[j], Lᵢ[j + m/2]) maps to Lᵢ₊₁[j] +# +# For the FRI verifier, the key precomputed datum per pair is: +# +# pair_diff_inv[i][j] = 1 / (Lᵢ[j + m/2] − Lᵢ[j]) +# +# This is the only thing beyond the domain points themselves that the verifier +# needs. The normalization s^e depends on the degree bound and is computed at +# fold time. + +class EcfftDomain: + def __init__(self, params, log_n): + assert log_n <= params['k'] + n = 1 << log_n + self.log_n = log_n + self.n = n + + # Set up the base curve and generator + curve = GoodCurve(params['a'], params['bb']) + gen = Point(params['gx'], params['gy'], curve) + + # Scale the generator so it has order exactly 2^log_n. + # The raw generator has order 2^k; multiplying by 2^{k − log_n} gives + # a point of order 2^log_n. + scaled_gen = gen.scalar_mul(1 << (params['k'] - log_n)) + + # Build the isogeny chain: k rational maps and k+1 curves + self.psis, self.curves, self.hs = build_isogeny_chain(scaled_gen, log_n) + + # Build L₀: x-coordinates of the coset {offset + i·scaled_gen | i = 0,…,n−1}. + # The offset must avoid producing any point with x = 0 (the 2-torsion + # point T = (0,0) is in the isogeny kernel). + # + # We use gen itself as the offset. When k > log_n, gen is NOT a multiple + # of scaled_gen (gen has order 2^k, scaled_gen has order 2^log_n, so gen + # is not in the subgroup ). This ensures the coset doesn't + # contain the identity or 2-torsion points. + # + # When k == log_n, gen = scaled_gen, so we use gen.double() as fallback + # (this works for small domains but may hit x=0 for large ones; k > log_n + # is recommended for production use). + if params['k'] > log_n: + coset = gen + else: + coset = gen.double() + L0 = [] + acc = Point.infinity(curve) + for _ in range(n): + pt = coset + acc + L0.append(pt.x) + acc = acc + scaled_gen + + # Build remaining layers by applying ψᵢ to each layer + self.layers = [L0] + current = L0 + for i in range(log_n): + psi = self.psis[i] + half = len(current) // 2 + next_layer = [psi(current[j]) for j in range(half)] + # Verify the 2-to-1 pairing: ψ(first half) == ψ(second half) + for j in range(half): + assert next_layer[j] == psi(current[j + half]), \ + f"ψ pairing broken at layer {i}, index {j}" + self.layers.append(next_layer) + current = next_layer + + # Precompute 1/(s₁ − s₀) for every pair in every layer + self._precompute_twiddles() + + def _precompute_twiddles(self): + """For each layer i and pair j, compute 1/(Lᵢ[j + m/2] − Lᵢ[j]). + + These are the only per-pair constants the verifier needs (beyond the + domain points). We batch-invert for efficiency. + """ + self.pair_diff_inv = [] + for i in range(self.log_n): + layer = self.layers[i] + half = len(layer) // 2 + diffs = [fsub(layer[j + half], layer[j]) for j in range(half)] + self.pair_diff_inv.append(batch_inv(diffs)) + + def num_rounds(self): + return self.log_n + + def layer(self, i): + return self.layers[i] + + def layer_size(self, i): + return len(self.layers[i]) + + def pair_at(self, round_idx, j): + """Return (s₀, s₁) for pair j at the given round.""" + layer = self.layers[round_idx] + half = len(layer) // 2 + return layer[j], layer[j + half] + + +# ============================================================================ +# ECFFT FRI fold — the "pointwise hash" from BSCKL22 Appendix B.2 +# ============================================================================ + +def ecfri_fold_step(word, domain, round_idx, degree_bound, z): + """One round of the ECFFT FRI fold. + + Given evaluations `word` on domain layer `round_idx` (size m), fold them + using challenge z and the stated degree_bound: + + For each pair j: + s₀, s₁ = Lᵢ[j], Lᵢ[j + m/2] + e = degree_bound/2 − 1 + a = word[j] / s₀ᵉ (normalize) + b = word[j + m/2] / s₁ᵉ + out[j] = a + (b − a)/(s₁ − s₀) · (z − s₀) + + Returns m/2 values: the evaluations on the next layer. + """ + layer = domain.layer(round_idx) + m = len(layer) + assert len(word) == m + assert degree_bound % 2 == 0 + assert degree_bound <= m + half = m // 2 + e = degree_bound // 2 - 1 + + diff_inv = domain.pair_diff_inv[round_idx] + out = [0] * half + + for j in range(half): + s0 = layer[j] + s1 = layer[j + half] + + # Normalize by v(s)ᵉ = sᵉ. When e = 0, normalization is trivial. + if e == 0: + a = word[j] + b = word[j + half] + else: + a = fdiv(word[j], fpow(s0, e)) + b = fdiv(word[j + half], fpow(s1, e)) + + # Interpolate the line through (s₀, a) and (s₁, b), evaluate at z + slope = fmul(fsub(b, a), diff_inv[j]) + out[j] = fadd(a, fmul(slope, fsub(z, s0))) + + return out + + +def ecfri_fold(word, domain, degree_bound, challenges): + """Multi-round fold: apply ecfri_fold_step for each challenge in sequence, + halving the degree bound each round.""" + current = list(word) + d = degree_bound + for i, z in enumerate(challenges): + current = ecfri_fold_step(current, domain, i, d, z) + d //= 2 + return current + + +def ecfri_verify_query(domain, round_idx, degree_bound, j, f_s0, f_s1, z): + """Verifier-side fold for a single query. + + Given the openings f(s₀) and f(s₁) at pair index j, compute the expected + fold value at Lᵢ₊₁[j]. This is exactly the same formula as ecfri_fold_step + but for a single pair. + """ + layer = domain.layer(round_idx) + half = len(layer) // 2 + e = degree_bound // 2 - 1 + + s0 = layer[j] + s1 = layer[j + half] + diff_inv = domain.pair_diff_inv[round_idx][j] + + if e == 0: + a, b = f_s0, f_s1 + else: + a = fdiv(f_s0, fpow(s0, e)) + b = fdiv(f_s1, fpow(s1, e)) + + slope = fmul(fsub(b, a), diff_inv) + return fadd(a, fmul(slope, fsub(z, s0))) + + +# ============================================================================ +# Lagrange interpolation (used only by test vector generation) +# ============================================================================ + +def lagrange_interp_coeffs(xs, ys): + """Recover polynomial coefficients from point evaluations. + + Given (x₀,y₀), …, (x_{n−1}, y_{n−1}), returns [c₀, c₁, …, c_{n−1}] such + that c₀ + c₁·x + ⋯ = yᵢ at each xᵢ. O(n²) — fine for small test sizes. + """ + n = len(xs) + assert len(ys) == n + coeffs = [0] * n + for i in range(n): + # Build Lagrange basis Lᵢ(x) = ∏_{j≠i} (x − xⱼ) / (xᵢ − xⱼ) + basis = [1] + denom = 1 + for j in range(n): + if j == i: + continue + denom = fmul(denom, fsub(xs[i], xs[j])) + new_basis = [0] * (len(basis) + 1) + for k in range(len(basis)): + new_basis[k] = fadd(new_basis[k], fmul(basis[k], fneg(xs[j]))) + new_basis[k + 1] = fadd(new_basis[k + 1], basis[k]) + basis = new_basis + denom_inv = finv(denom) + for k in range(len(basis)): + coeffs[k] = fadd(coeffs[k], fmul(ys[i], fmul(basis[k], denom_inv))) + return coeffs + + +def poly_degree(coeffs): + """Degree of a polynomial (index of highest nonzero coefficient, or −1).""" + for i in range(len(coeffs) - 1, -1, -1): + if coeffs[i] % q != 0: + return i + return -1 + + +# ============================================================================ +# Export: C++ header +# ============================================================================ + +def field_to_hex(val): + """Field element → 0x-prefixed 64-char big-endian hex string.""" + return "0x" + format(val % q, '064x') + + +def export_verifier_data_hpp(domain, outfile): + """Write a C++ header containing all domain points and pair-diff inverses. + + For each layer i (size m), emits: + - LAYER_i: m field elements (the x-coordinates Lᵢ[0..m)) + - PAIR_DIFF_INV_i: m/2 field elements (1/(s₁ − s₀) for each pair) + """ + log_n = domain.log_n + n = domain.n + + with open(outfile, 'w') as f: + f.write("#pragma once\n") + f.write("// AUTO-GENERATED by ecfft_precompute.py\n") + f.write(f"// Domain size: 2^{log_n} = {n}\n") + f.write(f"// Rounds: {log_n}\n\n") + f.write("#include \n") + f.write("#include \n\n") + f.write("namespace bb::basefold::domain_data {\n\n") + f.write(f"static constexpr size_t LOG_N = {log_n};\n") + f.write(f"static constexpr size_t N = {n};\n") + f.write(f"static constexpr size_t NUM_ROUNDS = {log_n};\n\n") + + for i in range(log_n + 1): + layer = domain.layer(i) + m = len(layer) + f.write(f"// Layer {i}: size {m}\n") + f.write(f"static constexpr std::array LAYER_{i} = {{\n") + for idx, x in enumerate(layer): + comma = "," if idx < m - 1 else "" + f.write(f' "{field_to_hex(x)}"{comma}\n') + f.write("};\n\n") + + if i < log_n: + half = m // 2 + diff_inv = domain.pair_diff_inv[i] + f.write(f"static constexpr std::array PAIR_DIFF_INV_{i} = {{\n") + for idx, x in enumerate(diff_inv): + comma = "," if idx < half - 1 else "" + f.write(f' "{field_to_hex(x)}"{comma}\n') + f.write("};\n\n") + + f.write("} // namespace bb::basefold::domain_data\n") + + print(f"Wrote verifier data to {outfile}") + + +# ============================================================================ +# Export: binary domain (compact, loaded by EcfftDomain::load_binary) +# ============================================================================ + +def export_binary(domain, outfile): + """Write the domain as a binary file loadable by EcfftDomain::load_binary(). + + Format: + u32 log_n + u32 n + u32 num_rounds + For each layer i = 0..num_rounds: + u32 m (layer size) + m × (4 × u64 LE) domain points (Montgomery form limbs) + If i < num_rounds: + m/2 × (4 × u64 LE) pair_diff_inv values + """ + import struct + log_n = domain.log_n + n = domain.n + + def fq_to_bytes(x): + """Convert field element to 4 little-endian u64s (Montgomery form).""" + v = int(x) % q + limbs = [] + for _ in range(4): + limbs.append(v & ((1 << 64) - 1)) + v >>= 64 + return struct.pack('<4Q', *limbs) + + with open(outfile, 'wb') as f: + f.write(struct.pack(' 8 else 0 + trace = [] + current_check = list(mr_evals) + d_check = n + idx = query_idx + for i in range(log_n): + layer = domain.layer(i) + m = len(layer) + half = m // 2 + j = idx if idx < half else idx - half + + s0, s1 = layer[j], layer[j + half] + f_s0, f_s1 = current_check[j], current_check[j + half] + expected_fold = ecfri_verify_query(domain, i, d_check, j, + f_s0, f_s1, alphas[i]) + + current_check = ecfri_fold_step(current_check, domain, i, d_check, + alphas[i]) + d_check //= 2 + actual_fold = current_check[j] + + trace.append({ + 'round': i, + 'pair_index': j, + 's0': field_to_hex(s0), + 's1': field_to_hex(s1), + 'f_s0': field_to_hex(f_s0), + 'f_s1': field_to_hex(f_s1), + 'expected_fold': field_to_hex(expected_fold), + 'actual_fold': field_to_hex(actual_fold), + 'match': expected_fold == actual_fold, + }) + idx = j + + test5 = { + 'name': 'verifier_query_trace', + 'initial_query_index': query_idx, + 'trace': trace, + 'pass': all(t['match'] for t in trace), + } + + data = { + 'log_n': log_n, + 'n': n, + 'tests': [test1, test2, test3, test4, test5], + } + + with open(outfile, 'w') as f: + json.dump(data, f, indent=2) + print(f"Wrote test vectors to {outfile}") + for t in data['tests']: + status = 'PASS' if t.get('pass', True) else 'FAIL' + print(f" {t['name']}: {status}") + + +# ============================================================================ +# Curve parameters +# ============================================================================ +# +# These define a point of order 2^19 on a good curve over Fq. They were +# computed by the ecfft-python library (searching for a curve with a large +# 2-power-order subgroup). We can build domains of size up to 2^19 from this. + +PARAMS_2_19 = { + 'a': 6447527507284313751241433169560096957243528132408891969270135568370318968332, + 'bb': 7045312493309525668369461691598070878448993674248805428332703556156728537593, + 'gx': 4829453208919067402500883847334816322244746888452843309002692632160138992140, + 'gy': 18907995215076895123251380389023476008045611862057851369609315058508855857578, + 'k': 19, +} + +PARAMS_2_20 = { + 'a': 20433470950457459813605429059280825836446698010609137768462731001356963008044, + 'bb': 4017660946671215398527212635155410452720141402746418430734298542572364476330, + 'gx': 72741324573834444051481096350144623350770302488412565367468391337396010612, + 'gy': 1735668802666030810767374651871167187380082049253283735063130026458754200195, + 'k': 20, +} + +# Found by the ecfft Rust crate (examples/find_curve_bn254_large.rs). +# Multiple candidates; use the one whose isogeny chain extends farthest. +PARAMS_2_22_CANDIDATES = [ + { + 'a': 14925974028831440472498989498448874428493852729505580740086939166720736498629, + 'bb': 9549790640130809530714484031949839461498822953635544459671498093855825339498, + 'gx': 360294937729954436130007569400096323964166489753278118353673070238393387007, + 'gy': 9719321663490340041323268301967550884932495756583379914404360206753317110942, + 'k': 22, + }, + { + 'a': 19431937952045497325475941205273591144061550730583066743028841034083879357342, + 'bb': 10202060775725426090529509036517724253342864395770849203826939727879552035254, + 'gx': 6986920228258601407553270092689791716122346226109788942087357003069729735907, + 'gy': 14795898283168152019831350641736646309961800052222572354541901743934809485514, + 'k': 22, + }, + { + 'a': 7707892530415032609275864774651616491723025938262112330135615880565858296768, + 'bb': 4621039616553694309793664698429570572960623031453700747375451010989940605298, + 'gx': 18287844914400233271758987982762077448596673014934826567190911847360400107318, + 'gy': 3785840349822604081360343532146726998560798244112488404268692654634977278128, + 'k': 22, + }, +] +PARAMS_2_22 = PARAMS_2_22_CANDIDATES[1] # Candidate 1 supports 20-level isogeny chain + + +# ============================================================================ +# CLI +# ============================================================================ + +def main(): + parser = argparse.ArgumentParser( + description="Generate ECFFT domain data for BaseFold") + parser.add_argument('--log-n', type=int, default=8, + help='Log₂ of domain size (default: 8)') + parser.add_argument('--output-hpp', type=str, default=None, + help='Output C++ header file path') + parser.add_argument('--output-test', type=str, default=None, + help='Output test vectors JSON file path') + parser.add_argument('--output-bin', type=str, default=None, + help='Output binary domain file (for load_binary)') + args = parser.parse_args() + + log_n = args.log_n + # Pick the smallest parameter set with k > log_n (need k > log_n to avoid + # the isogeny kernel when constructing the coset). + if log_n <= 17: + params = PARAMS_2_19 + elif log_n <= 19: + params = PARAMS_2_20 + elif log_n <= 21: + params = PARAMS_2_22 + else: + raise ValueError(f"log_n={log_n} exceeds maximum supported (21)") + + assert log_n <= params['k'], \ + f"Requested log_n={log_n} exceeds available subgroup order 2^{params['k']}" + + print(f"Building ECFFT domain for log_n = {log_n} " + f"(domain size = {1 << log_n}), using 2^{params['k']} subgroup...") + domain = EcfftDomain(params, log_n) + print(f" {domain.num_rounds()} rounds, " + f"layers: {[domain.layer_size(i) for i in range(log_n + 1)]}") + + if args.output_bin: + export_binary(domain, args.output_bin) + + if args.output_hpp: + export_verifier_data_hpp(domain, args.output_hpp) + + if args.output_test: + export_test_vectors(domain, args.output_test) + + if not args.output_hpp and not args.output_test: + export_test_vectors(domain, f"basefold_test_vectors_{log_n}.json") + + +if __name__ == '__main__': + main() diff --git a/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_test_vectors_2_8.json b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_test_vectors_2_8.json new file mode 100644 index 000000000000..3f3f7f36604c --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/msm_verification/basefold/ecfft_test_vectors_2_8.json @@ -0,0 +1,281 @@ +{ + "log_n": 8, + "n": 256, + "tests": [ + { + "name": "degree_halving_rate1", + "n": 256, + "degree_bound": 256, + "challenge": "0x1c32bf76fc043f0892070158f2771f63ada584175e2ad32d31ab0b56d5c9fdc8", + "input_degree": 255, + "output_degree": 127, + "output_degree_bound": 128, + "pass": true + }, + { + "name": "degree_halving_rate_quarter", + "n": 256, + "degree_bound": 64, + "challenge": "0x027d91010b9900340a4e255280051b1066c14dca8255da619a3b031932f57511", + "input_degree": 63, + "output_degree": 31, + "output_degree_bound": 32, + "pass": true + }, + { + "name": "locality", + "n": 256, + "degree_bound": 256, + "challenge": "0x09883e6a74fd33d184f2fd0f79474bfacdd0b4d4554227deb6adf48b2314d0c9", + "checks": [ + { + "j": 0, + "expected": "0x27c9b6608e07c83dbcd2ada0591b93a89849456f755f607d80db5695c31e9ffc", + "local_computed": "0x27c9b6608e07c83dbcd2ada0591b93a89849456f755f607d80db5695c31e9ffc", + "match": true + }, + { + "j": 1, + "expected": "0x0344e43daad12f7057d0707daee68aa8ea3e14546121638f7fa270abd82cc44e", + "local_computed": "0x0344e43daad12f7057d0707daee68aa8ea3e14546121638f7fa270abd82cc44e", + "match": true + }, + { + "j": 2, + "expected": "0x01a09796e52cd8ad308133df45cee82760bfc1c56a2641b5697eca8a44627487", + "local_computed": "0x01a09796e52cd8ad308133df45cee82760bfc1c56a2641b5697eca8a44627487", + "match": true + }, + { + "j": 3, + "expected": "0x244c5a8f413a653583cc01b7cff1108e77086e42282db8e6eb5bf6229d3875f2", + "local_computed": "0x244c5a8f413a653583cc01b7cff1108e77086e42282db8e6eb5bf6229d3875f2", + "match": true + }, + { + "j": 4, + "expected": "0x2adc5f2d6d4fd118104c1631b22d1d054bdd303d017e16f76bffe1fd9ca3b14a", + "local_computed": "0x2adc5f2d6d4fd118104c1631b22d1d054bdd303d017e16f76bffe1fd9ca3b14a", + "match": true + }, + { + "j": 5, + "expected": "0x2a777f1b093ea24b549c4f4260c8f4704081134bcd1f8ac4b60e26fd8880dc7d", + "local_computed": "0x2a777f1b093ea24b549c4f4260c8f4704081134bcd1f8ac4b60e26fd8880dc7d", + "match": true + }, + { + "j": 6, + "expected": "0x21ca2699a61eae664cb0789d420fbfb89ae4b6e5ba292fb415b65194d9805372", + "local_computed": "0x21ca2699a61eae664cb0789d420fbfb89ae4b6e5ba292fb415b65194d9805372", + "match": true + }, + { + "j": 7, + "expected": "0x15458691e2c453143c201befa0a1a0b352ba208744af26b529b277c896994900", + "local_computed": "0x15458691e2c453143c201befa0a1a0b352ba208744af26b529b277c896994900", + "match": true + } + ], + "pass": true + }, + { + "name": "multi_round_fold", + "n": 256, + "num_rounds": 8, + "challenges": [ + "0x19447986143665f54c81926079ec533c3c53090ee1b41171929d674d798e1760", + "0x21138942e684390c923b78208089b8c508e3f771b6dcbd05da9bccc1e71c681b", + "0x0d8f9bbdf8dfcc3a2acb4fbdfbff8a7df2f5b1762c41cbd372f444abc1d71f4b", + "0x2bb853e1fcffe9e3140ff5896c624c2c0aeccec7204db9da31567f2acef7482a", + "0x261542c7d5881c1c25db5946a091e5b4d2938e6eff4d093033b0fb536f75ee8b", + "0x0591db8f1127b207ceae1325ea074d80ba236c22e92a7b9751db1a374204d734", + "0x2142923546b518dedb4bb8eb537d0bd18d536c7565b90e948e6de263635a775e", + "0x259ad8ba9f80b96bb34e356ac95b61f8032a461adf9b7c1feacb1f6d7549c9fa" + ], + "initial_evals": [ + "0x2443132d25f06b0a264ef59d6a18e5af1cce08e96b6a8061c81c8d3185c69fc0", + "0x06c2b88becc817a619a6f80ef08ce6c2f6ff3521c2250530952a53459682da00", + "0x249c07587c4f48e4a7a02a3d620f230b439e754c693016eae0bda1ee5807cf75", + "0x2791515724d3875167294d3c46818a882c154804099759a37919955c9be4f1a0", + "0x11307c6debbbd76379a7e63bbee975da0c9e55503cd1ff3fb0c5827854dccba6", + "0x13e697e64d950273cb2cc805d1403430567e642e052dfb9a5f584603e5a3d8ea", + "0x05b6e036a543148e9adfe9816ab19e15ba92992e44f8b53c52246d05d3692868", + "0x1f0165e6bdae8eff683c510ed2efa93be9d6bb7e361db014711456de301c8db4" + ], + "rounds": [ + { + "round": 0, + "size": 128, + "first_values": [ + "0x12f618f89deb8c9eb9e5163be8d69535ae1e5b34b771337ff8012d7b75293bb5", + "0x1d77b955933466fa5012ee36ead2e716ac7bf0062bc97001b265c8835107b6cd", + "0x027d18f4ab077d0465504d136eb2eb4f5f5fdbafd15c8cb3133792f8d9565c92", + "0x205b2662364591fc828704761675328d90b4ec2d24677cdc18bf625608f979bb" + ] + }, + { + "round": 1, + "size": 64, + "first_values": [ + "0x00648ef7b4841c80d685c017fbf16e9929ff6cd707dd74f59fd9c908c55302cb", + "0x18751d054974662dc720fe6657d8af7aac05fd8f37b3334248ffd603dd91e1b6", + "0x00fa765b086931f8266d561a2af834df50ca4a7fcb10764b5392af496e5fa5b3", + "0x2560e6ff88ef4f14b491cf4f76c5e3a2aa018a7d7011a0eb5625ac8fde57fa68" + ] + }, + { + "round": 2, + "size": 32, + "first_values": [ + "0x14ca729f0ac9f246ed998f16dd7c9bef8ee7e3b163288ff83c23ab9ba20dda82", + "0x0ebbcd49fdd498adcd7e1bc7b0d801cb918ef3193a01521a4d39062ffd2ab735", + "0x0da25fec5e8866890019b8939beaf4a06e8377ed5b872342d97ad0f51686ac6c", + "0x0204b967c157ff6986000ba543d017dbea62c29b60ba64832ed714ceb934b288" + ] + }, + { + "round": 3, + "size": 16, + "first_values": [ + "0x25d7489c55eca7d29596d5e322da06713dbf53766f3e3d37d0fabef470b05d28", + "0x1f686c43758c81980fa4d06b3c683bfed2d00f1032da2c8403812432624c3ff8", + "0x28a5d6c8a7ab00ee6b36560e3868525ca668006711257f711ccd64ac72837b7c", + "0x0bc9d91cd1a784cc93f936dff03fbf28c7233e8cb887c0e467951edab0d3ebda" + ] + }, + { + "round": 4, + "size": 8, + "first_values": [ + "0x22217658485c20f23ab3d50afd1c58e774efe2b835b7f1678411fadd480a24aa", + "0x116da1a856bf6542ea8e67c7b4c28e7f398822991bbe9122db233b81f1187128", + "0x0692b9b32b1521b04e8a91b1a239855f55b4804ecbdbccda0acee9f43ac71115", + "0x1b1dcd09eb9e90752edefb0fc2cef6a2e3167b3c8cb50784d7cf776bc0c8307b" + ] + }, + { + "round": 5, + "size": 4, + "first_values": [ + "0x08f00ab4a2fe74f10d66accba0d2881362d8e345bbc86f88e3d4a237279741f0", + "0x0ec6462c721e8f17e74e81411ad399cf2e4846e65f09be30d638459c5bad3b2d", + "0x07cd9ba758eeae51e48d665991d5f43be3a102aa222cd531154bc61d3f723374", + "0x2416773a2ecd3bf414e3c8c1a5cf7a3528f632d4f089a4779086641f6b21851b" + ] + }, + { + "round": 6, + "size": 2, + "first_values": [ + "0x0110758e492bf15875edf9bc7cc22ea32de6d603efdb3c98e074ba435224f16d", + "0x29c53dd7688c55e5cbdf03eca19fa61902aca4de21feede231dd6391091f2676" + ] + }, + { + "round": 7, + "size": 1, + "first_values": [ + "0x2f00eecd607175a77f8eeb5b565ffe2974214ca8f5ab43f495127351e8d807dc" + ] + } + ], + "final_value": "0x2f00eecd607175a77f8eeb5b565ffe2974214ca8f5ab43f495127351e8d807dc" + }, + { + "name": "verifier_query_trace", + "initial_query_index": 3, + "trace": [ + { + "round": 0, + "pair_index": 3, + "s0": "0x0748ed2088f66b6ac8a17591870796f4b8cf1771cabf903593661c1fa469a7f4", + "s1": "0x0927fd5f5593479417b91cc1f9588c9321507cbb2b47fc640105d3118af1857e", + "f_s0": "0x2791515724d3875167294d3c46818a882c154804099759a37919955c9be4f1a0", + "f_s1": "0x090c44ff8e190d07dff0b9bf8db3c2c102dabd76f3102525f5179e725f857b22", + "expected_fold": "0x205b2662364591fc828704761675328d90b4ec2d24677cdc18bf625608f979bb", + "actual_fold": "0x205b2662364591fc828704761675328d90b4ec2d24677cdc18bf625608f979bb", + "match": true + }, + { + "round": 1, + "pair_index": 3, + "s0": "0x0802844703c620a6691842c473e2086d79888d0703184dc9b50ba04a7b201eed", + "s1": "0x2abf8ec986430646ca066c3424a5de7e31c2503bfea685f68823adb9d4b22ac0", + "f_s0": "0x205b2662364591fc828704761675328d90b4ec2d24677cdc18bf625608f979bb", + "f_s1": "0x244c9e269fb7cc784400a81c2463fc9d39dfa302373be4747bae939e667b7edc", + "expected_fold": "0x2560e6ff88ef4f14b491cf4f76c5e3a2aa018a7d7011a0eb5625ac8fde57fa68", + "actual_fold": "0x2560e6ff88ef4f14b491cf4f76c5e3a2aa018a7d7011a0eb5625ac8fde57fa68", + "match": true + }, + { + "round": 2, + "pair_index": 3, + "s0": "0x07c1f7473a3724879cd131fd570cbf2de71207bd3444c93b745ef029dd319d08", + "s1": "0x15148bd1babd56dfd185c8beb5843837dbf1274118220d212c467a0f51ce2a02", + "f_s0": "0x2560e6ff88ef4f14b491cf4f76c5e3a2aa018a7d7011a0eb5625ac8fde57fa68", + "f_s1": "0x1700215d2aefe808f5c6e7cc6be06b7ef183658fa22925accea69d0eaec41a65", + "expected_fold": "0x0204b967c157ff6986000ba543d017dbea62c29b60ba64832ed714ceb934b288", + "actual_fold": "0x0204b967c157ff6986000ba543d017dbea62c29b60ba64832ed714ceb934b288", + "match": true + }, + { + "round": 3, + "pair_index": 3, + "s0": "0x2bedd3efac6372eb8ceb2a9853dd895587dba39dbb61ddef599b514bfb6c6d10", + "s1": "0x08bd1881e51c65b11ad8db6b2c595d293df2c623eb1a0288387cdac4bdba5d9a", + "f_s0": "0x0204b967c157ff6986000ba543d017dbea62c29b60ba64832ed714ceb934b288", + "f_s1": "0x26504b973c386bd20a1f21eaac41503f8ab97a974b64439225f717336681f643", + "expected_fold": "0x0bc9d91cd1a784cc93f936dff03fbf28c7233e8cb887c0e467951edab0d3ebda", + "actual_fold": "0x0bc9d91cd1a784cc93f936dff03fbf28c7233e8cb887c0e467951edab0d3ebda", + "match": true + }, + { + "round": 4, + "pair_index": 3, + "s0": "0x2a00c593ce33b6ae4a4d44a37ff9895f9642acfba92cc8cabe5898288a377dc9", + "s1": "0x123ac98cabb482b25cf6ae93f15458d3d845870152ac15b189bd504b35395b1a", + "f_s0": "0x0bc9d91cd1a784cc93f936dff03fbf28c7233e8cb887c0e467951edab0d3ebda", + "f_s1": "0x2b6ef9e841c580067ae2d876e9de9904761091f89cb8aaf229a72486495e5d8d", + "expected_fold": "0x1b1dcd09eb9e90752edefb0fc2cef6a2e3167b3c8cb50784d7cf776bc0c8307b", + "actual_fold": "0x1b1dcd09eb9e90752edefb0fc2cef6a2e3167b3c8cb50784d7cf776bc0c8307b", + "match": true + }, + { + "round": 5, + "pair_index": 3, + "s0": "0x17abebbe659e61dff0636196911a19a36a6b793fba8cf980b91cbe094898d672", + "s1": "0x1e02b1e5312ef7e3452f0683cd6f1dd014d1a213c4c6bd8f9082a956003cdd47", + "f_s0": "0x1b1dcd09eb9e90752edefb0fc2cef6a2e3167b3c8cb50784d7cf776bc0c8307b", + "f_s1": "0x24c1ef37c0d7e30f96103e1f9e3cd1661330c95493030f6ce7860aba0c7d577c", + "expected_fold": "0x2416773a2ecd3bf414e3c8c1a5cf7a3528f632d4f089a4779086641f6b21851b", + "actual_fold": "0x2416773a2ecd3bf414e3c8c1a5cf7a3528f632d4f089a4779086641f6b21851b", + "match": true + }, + { + "round": 6, + "pair_index": 1, + "s0": "0x11185856fd8c30d0bd544caf151722a29b6151f01cd0be969542662449c0d153", + "s1": "0x070c299af10bc6cf5ca7fafa3bda07c65cd76dd454152ef7f297ccf89e5c548a", + "f_s0": "0x0ec6462c721e8f17e74e81411ad399cf2e4846e65f09be30d638459c5bad3b2d", + "f_s1": "0x2416773a2ecd3bf414e3c8c1a5cf7a3528f632d4f089a4779086641f6b21851b", + "expected_fold": "0x29c53dd7688c55e5cbdf03eca19fa61902aca4de21feede231dd6391091f2676", + "actual_fold": "0x29c53dd7688c55e5cbdf03eca19fa61902aca4de21feede231dd6391091f2676", + "match": true + }, + { + "round": 7, + "pair_index": 0, + "s0": "0x162bda1ccb9a43f388bb4ae9ecc88b18be5528f83e18e3292ebac419eac9b7e9", + "s1": "0x2ede82c8a583f14753c8634ddb24d22b559dcb540ec5973dfdd9083798faefc8", + "f_s0": "0x0110758e492bf15875edf9bc7cc22ea32de6d603efdb3c98e074ba435224f16d", + "f_s1": "0x29c53dd7688c55e5cbdf03eca19fa61902aca4de21feede231dd6391091f2676", + "expected_fold": "0x2f00eecd607175a77f8eeb5b565ffe2974214ca8f5ab43f495127351e8d807dc", + "actual_fold": "0x2f00eecd607175a77f8eeb5b565ffe2974214ca8f5ab43f495127351e8d807dc", + "match": true + } + ], + "pass": true + } + ] +} \ No newline at end of file