Skip to content

fix: non-native constant ops fast paths#1730

Merged
ivokub merged 6 commits intomasterfrom
fix/constant-ops
Mar 11, 2026
Merged

fix: non-native constant ops fast paths#1730
ivokub merged 6 commits intomasterfrom
fix/constant-ops

Conversation

@ivokub
Copy link
Copy Markdown
Collaborator

@ivokub ivokub commented Mar 11, 2026

Description

Issue found by Diligence fuzzer. Thanks @wuestholz for reporting. Agent summary of the issue (and human-confirmed independently):

C2 differs from C1 only by replacing exprA with field_api.Exp(exprA, field_api.One()) in examples/fuzzing/fuzzing_test.go:545. Running the provided test shows C2 fails during compilation with:

trying to reduce a constant, which happen to have an overflow flag set

The failure path is:
std/math/emulated/field_mul.go:769 Exp
-> repeated squaring via std/math/emulated/field_mul.go:817 Mul
-> overflow handling in std/math/emulated/field_ops.go:411 reduceAndOp
-> panic in std/math/emulated/field_reduce.go:32.

The root cause is an internal invariant break in the non-native gadget:

  • Exp uses a fixed-window algorithm, so even exponent 1 is processed over the full bit width of the field.
  • In the high zero windows, the accumulator is the constant 1, and small-field Mul uses non-reducing multiplication by default.
  • That multiplication preserves the arithmetic value as a compile-time constant, but still increases the overflow metadata.
  • Later, overflow handling asks Reduce to reduce that element.
  • Reduce assumes constants can never carry overflow and panics if constantValue(a) is true while a.overflow > 0.

So the bug is fundamentally: operations can produce constant-valued Elements with non-zero overflow, but Reduce treats that state as impossible.

Implemented regression tests for particular case, also added constant fast path handling to other methods as well.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

How has this been tested?

Added regression tests and constant path tests.

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 non-native arithmetic paths (Mul*, Div, Inverse, Sqrt) and changes when hints/deferred checks are emitted, so incorrect constant detection/mod arithmetic could alter constraint generation, though changes are gated to compile-time constants and covered by new regressions.

Overview
Fixes a small-field emulation invariant where operations could return constant-valued elements with non-zero overflow, later triggering a panic in Reduce (e.g., during Exp(x, 1) compilation).

Adds compile-time constant fast paths for Mul/MulNoReduce (including mulMod when p == nil) plus Div, Inverse, and Sqrt, computing results via big.Int mod arithmetic and returning newConstElement without hints/deferred checks or overflow growth. New unit/regression tests cover constant fast paths across small/large fields and the fuzz-found Exp(x,1) / Mul(1,1) compilation regressions.

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

@ivokub ivokub added type: bug Something isn't working src: fuzzing Issue found using a fuzzing tool feat: emulated labels Mar 11, 2026
Copy link
Copy Markdown

@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 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Comment thread std/math/emulated/field_ops.go
Copy link
Copy Markdown

@YaoJGalteland YaoJGalteland left a comment

Choose a reason for hiding this comment

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

The constant overflow issue fix is correct and the tests are solid. The main concerns is: Exp still wasting 14 multiplications for trivial constant exponents. For example, Exp(x, Zero()) and Exp(x, One()) has no fast paths.

Clauded some analysis (see below), looks pretty promising. Does that logic make sense to you?

The current implementation uses a fixed 4-bit window over all 64 bits, always:

Component Count
Table building (table[2..15]) 14 MulMod
Squarings (16 windows × 4) 64 Mul
Table-lookup multiplications (16 windows × 1) 16 Mul
Total 94

This is constant regardless of the exponent value.

Cost: (bitLen(exp) − 1) squarings + (popcount(exp) − 1) multiplications.

Cost comparison (Goldilocks, 64-bit field)

Exponent Before After (sq + mul) Savings Ratio
exp = 0 0 0 (0 + 0) 0
exp = 1 94 0 (0 + 0) 94
exp = 2 94 1 (1 + 0) 93 94×
exp = 3 94 2 (1 + 1) 92 47×
exp = 7 94 4 (2 + 2) 90 23.5×
exp = 255 94 14 (7 + 7) 80 6.7×
exp = 2³² (Frobenius) 94 32 (32 + 0) 62 2.9×
exp = 2⁶³ 94 63 (63 + 0) 31 1.5×
exp = (p−1)/2 (Legendre) 94 93 (62 + 31) 1 ~1×
exp = p−1 (dense) 94 94 (63 + 31) 0

The worst case for binary s-a-m is a maximally dense exponent (p−1, all bits
set) where it ties with the windowed method. The windowed method's advantage is
for variable exponents, where it amortizes multiplications across 4-bit
windows. For constant exponents that advantage is lost since zero windows are
known at compile time.

The practically important cases (Frobenius, Legendre symbol, small exponents)
all fall in the 2×–∞ improvement range.

@ivokub
Copy link
Copy Markdown
Collaborator Author

ivokub commented Mar 11, 2026

The constant overflow issue fix is correct and the tests are solid. The main concerns is: Exp still wasting 14 multiplications for trivial constant exponents. For example, Exp(x, Zero()) and Exp(x, One()) has no fast paths.

Clauded some analysis (see below), looks pretty promising. Does that logic make sense to you?

The current implementation uses a fixed 4-bit window over all 64 bits, always:

Component Count
Table building (table[2..15]) 14 MulMod
Squarings (16 windows × 4) 64 Mul
Table-lookup multiplications (16 windows × 1) 16 Mul
Total 94
This is constant regardless of the exponent value.

Cost: (bitLen(exp) − 1) squarings + (popcount(exp) − 1) multiplications.

Cost comparison (Goldilocks, 64-bit field)

Exponent Before After (sq + mul) Savings Ratio
exp = 0 0 0 (0 + 0) 0 —
exp = 1 94 0 (0 + 0) 94
exp = 2 94 1 (1 + 0) 93 94×
exp = 3 94 2 (1 + 1) 92 47×
exp = 7 94 4 (2 + 2) 90 23.5×
exp = 255 94 14 (7 + 7) 80 6.7×
exp = 2³² (Frobenius) 94 32 (32 + 0) 62 2.9×
exp = 2⁶³ 94 63 (63 + 0) 31 1.5×
exp = (p−1)/2 (Legendre) 94 93 (62 + 31) 1 ~1×
exp = p−1 (dense) 94 94 (63 + 31) 0 1×
The worst case for binary s-a-m is a maximally dense exponent (p−1, all bits set) where it ties with the windowed method. The windowed method's advantage is for variable exponents, where it amortizes multiplications across 4-bit windows. For constant exponents that advantage is lost since zero windows are known at compile time.

The practically important cases (Frobenius, Legendre symbol, small exponents) all fall in the 2×–∞ improvement range.

Yup, good observation. Will implement in separate PR (ExpConst and ExpBits)

@ivokub ivokub merged commit a38399b into master Mar 11, 2026
13 checks passed
@ivokub ivokub deleted the fix/constant-ops branch March 11, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat: emulated src: fuzzing Issue found using a fuzzing tool type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants