Skip to content

Perf: LogUp* optimization for range checks and precomputed tables#1703

Closed
yelhousni wants to merge 6 commits intomasterfrom
feat/logup
Closed

Perf: LogUp* optimization for range checks and precomputed tables#1703
yelhousni wants to merge 6 commits intomasterfrom
feat/logup

Conversation

@yelhousni
Copy link
Contributor

@yelhousni yelhousni commented Feb 7, 2026

Summary

This PR implements the LogUp* optimization from eprint 2025/946 for indexed lookups in gnark. The key insight is that for lookups where query[i] = table[index[i]], we don't need to commit to query values since they equal table values at known indices.

This optimization applies to:

  1. Range checks - where table[i] = i (identity table) and limb values are indices
  2. Precomputed binary operations - XOR, AND, OR on bytes where table[x|y<<8] = f(x,y)

Constraint Improvements

Direct LogUp* Savings

The constraint savings equal exactly the number of queries (limbs):

Use Case Queries Constraints Saved
Range check (16 limbs) 16 16
Range check (100 limbs) 100 100
XOR operations (500 queries) 500 500

EVM Precompiles (SCS/PLONK)

Circuit Before After Saved Reduction
ECRecover 346,687 307,572 -39,115 11.3%
P256Verify 666,145 581,675 -84,470 12.7%
BN-ECAdd 5,477 4,725 -752 13.7%
BN-ECMul 210,369 183,009 -27,360 13.0%
BN-ECPair(2) 1,763,538 1,534,698 -228,840 13.0%
BLS-G1Add 9,414 8,022 -1,392 14.79%
BLS-G1MSM(10) 4,397,157 3,821,909 -575,248 13.08%
BLS-G2Add 21,645 18,173 -3,472 16.04%
BLS-G2MSM(10) 9,304,517 8,010,021 -1,294,496 13.91%
BLS-ECPair(2) 2,487,609 2,142,721 -344,888 13.86%
BLS-MapToG1 203,432 179,077 -24,355 11.97%
BLS-MapToG2 734,108 631,778 -102,330 13.94%

PLONK recursion (SCS/PLONK)

Circuit Before (LogUp) After (LogUp*) Saved Reduction (%)
Native PLONK (BLS12-377 in BW6-761) 252,060 244,683 -7,377 2.93%
Emulated PLONK (BW6-761 in BN254) 15,041,983 13,131,182 -1,910,801 12.70%

N.B.: the native PLONK recursion creates an emulated field for scalar operations, hence the ~3% saving.

Hash functions (SCS/PLONK)

benchmarks (64 bytes input):

Hash Function Before (LogUp) After (LogUp*) Saved Reduction (%)
SHA-256 691,934 669,183 -22,751 3.29%
Keccak-256 747,868 714,388 -33,480 4.48%
SHA3-256 747,868 714,388 -33,480 4.48%
RIPEMD-160 891,221 876,647 -14,574 1.64%

N.B.: The savings are more modest for hash functions (~2-4%) compared to EVM precompiles (~12-14%) because:

  1. Hash functions are dominated by permutation logic (rotations, additions, boolean ops), not lookups
  2. The XOR/AND/OR byte operations via logderivprecomp are only a fraction of total constraints
  3. The table size (65,536 for 2-byte XOR) is large relative to the number of queries per hash

Technical Details

LogUp Equation Reformulation

Original LogUp:

∑_{j∈table} count[j]/(X - table[j]) = ∑_{i∈queries} 1/(X - query[i])
  • Commits to: table (if variable), all queries, and multiplicities

LogUp* for indexed lookups:

∑_{j∈table} count[j]/(X - table[j]) = ∑_{i∈indices} 1/(X - table[index[i]])
  • Commits to: only multiplicities (table is constant, queries derived from indices)
  • Saves O(m) commitment elements for m queries

New API

// For identity tables [0,1,...,n-1] (range checks)
func BuildIndexedConstant(api frontend.API, tableSize int, indices []frontend.Variable) error

// For precomputed constant tables where table[i] = f(i)
func BuildIndexedPrecomputed(api frontend.API, table []*big.Int, indices []frontend.Variable, queryValues []frontend.Variable) error

Files Changed

File Changes
std/internal/logderivarg/logderivarg.go Added countIndexedHint, BuildIndexedConstant, BuildIndexedPrecomputed
std/internal/logderivarg/logderivarg_test.go New test file with unit tests and constraint comparison
std/internal/logderivprecomp/logderivprecomp.go Store indices, use BuildIndexedPrecomputed
std/internal/logderivprecomp/logderivprecomp_test.go Added constraint comparison test
std/rangecheck/rangecheck_commit.go Use BuildIndexedConstant

Testing

  • All existing tests pass (go test ./std/...)
  • New unit tests for BuildIndexedConstant and BuildIndexedPrecomputed
  • Tests cover: basic usage, repeated indices, single index, large tables, invalid indices, small field support
  • Constraint comparison tests verify savings match expected values

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • I did not modify files generated from templates
  • golangci-lint does not output errors locally
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Note

Medium Risk
Touches core proving constraint logic (log-derivative lookups, rangecheck commitments, and precomputed table verification), so mistakes could silently weaken soundness or break circuits, though changes are scoped and covered by new tests.

Overview
Implements the LogUp* optimization for indexed lookups by adding countIndexedHint plus new builders BuildIndexedConstant (identity tables) and BuildIndexedPrecomputed (constant precomputed tables) that commit only multiplicities, not per-query values.

Updates rangecheck commitments and logderivprecomp (byte XOR/AND/OR-style tables) to use the indexed builders by tracking query indices alongside packed query values, and adds targeted tests/bench-style constraint comparisons plus updated internal/stats/latest_stats.csv to reflect reduced PLONK/SCS constraint counts.

Written by Cursor Bugbot for commit 4e0d1bf. This will update automatically on new commits. Configure here.

@yelhousni yelhousni self-assigned this Feb 7, 2026
@yelhousni yelhousni added type: perf dep: linea Issues affecting Linea downstream labels Feb 7, 2026
@yelhousni yelhousni added this to the v0.14.N milestone Feb 7, 2026
@yelhousni yelhousni requested review from Copilot and ivokub February 8, 2026 02:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a LogUp* (“indexed lookup”) variant of the log-derivative argument to reduce commitment size/constraints for lookups where query values are derivable from a constant table and an index (notably range checks and precomputed byte ops).

Changes:

  • Adds countIndexedHint, BuildIndexedConstant, and BuildIndexedPrecomputed to logderivarg.
  • Updates rangecheck commitment flow to use BuildIndexedConstant for identity tables.
  • Updates precomputed binary-op lookups to track indices and use BuildIndexedPrecomputed, plus adds/extends tests and constraint-count comparisons.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
std/rangecheck/rangecheck_commit.go Switches rangecheck lookup to BuildIndexedConstant (LogUp*)
std/internal/logderivarg/logderivarg.go Adds indexed-hint + new LogUp* builders
std/internal/logderivarg/logderivarg_test.go Adds unit tests + constraint comparison for indexed builders
std/internal/logderivprecomp/logderivprecomp.go Stores packed indices and routes to BuildIndexedPrecomputed
std/internal/logderivprecomp/logderivprecomp_test.go Adds constraint comparison test for precomputed XOR lookups

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Copy link
Collaborator

@ivokub ivokub left a comment

Choose a reason for hiding this comment

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

I don't think the optimisation is valid:

  • either we should need to know the indices ahead of time
  • or the queries should be committed to previously

But none of these conditions hold imo. The values we query are provided by the hint, so they are not static (the approach would work in fixed-permutation argument though). In case of range checks we usually want to range check the values coming from a hint and in case of logderivprecomp the values come from precomputation hint.

And the values we're querying are neither committed to previously - for example in case of range checks they also come from the hint when we split larger limbs into smaller ones (64-bit to 4 16-bit limbs). We still add the constraints that the decompoistion is correct (v = v0 + v1 << 16 + v2 << 32 + v3 << 48), but it only reduces the search space to find valid solution.

See longer POC in DM. I'm not sure how to fix it here though, we still would have to commit to the query values. The only use case I see is when we want to implement fixed-permutation argument. This possibly has a use case i.e. we're doing scalar multiplication between constant scalar and variable point (then we could implement as mux-lookup). Perhaps also in final-exp computation in pairing? There we have a fixed exponent.

@yelhousni
Copy link
Contributor Author

I don't think the optimisation is valid:

  • either we should need to know the indices ahead of time
  • or the queries should be committed to previously

But none of these conditions hold imo. The values we query are provided by the hint, so they are not static (the approach would work in fixed-permutation argument though). In case of range checks we usually want to range check the values coming from a hint and in case of logderivprecomp the values come from precomputation hint.

And the values we're querying are neither committed to previously - for example in case of range checks they also come from the hint when we split larger limbs into smaller ones (64-bit to 4 16-bit limbs). We still add the constraints that the decompoistion is correct (v = v0 + v1 << 16 + v2 << 32 + v3 << 48), but it only reduces the search space to find valid solution.

See longer POC in DM. I'm not sure how to fix it here though, we still would have to commit to the query values. The only use case I see is when we want to implement fixed-permutation argument. This possibly has a use case i.e. we're doing scalar multiplication between constant scalar and variable point (then we could implement as mux-lookup). Perhaps also in final-exp computation in pairing? There we have a fixed exponent.

Indeed the pqper assumes indices are committed before the challenge, which does not seem to be in gnark's BSB22 workflow. A potential fix would be to make the BSB22 challenge derivation incorporate L, R, O (or equivalently, derive it from the Fiat-Shamir transcript after L, R, O are committed)? But that would require changing gnark's proving protocol flow, not just the lookup construction.

@yelhousni yelhousni marked this pull request as draft February 13, 2026 00:35
@yelhousni yelhousni closed this Feb 13, 2026
@yelhousni yelhousni deleted the feat/logup branch February 13, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dep: linea Issues affecting Linea downstream type: perf

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants