Skip to content

fix: handle non-modulus-width elements in emulated IsZero - #1806

Open
0x5ea000000 wants to merge 3 commits into
Consensys-Incorporated:masterfrom
0x5ea000000:fix/emulated-iszero-non-modulus-width
Open

fix: handle non-modulus-width elements in emulated IsZero#1806
0x5ea000000 wants to merge 3 commits into
Consensys-Incorporated:masterfrom
0x5ea000000:fix/emulated-iszero-non-modulus-width

Conversation

@0x5ea000000

@0x5ea000000 0x5ea000000 commented Aug 12, 2026

Copy link
Copy Markdown

Description

emulated.Field.IsZero assumed the element it reduces always comes back with exactly the modulus limb count. Elements built by Field.FromBits can legally carry a different number of limbs with zero overflow, and Field.Reduce's fast path returns zero-overflow elements unchanged regardless of width. This broke IsZero in two ways:

  • Wider than the modulus (e.g. a 512-bit FromBits result over a 4-limb modulus): the limb-wise ca == p comparison indexed out of range and panicked at circuit definition (index out of range [4] with length 4). This is reachable from std: scalarMulFakeGLV calls IsZero(s) on the incoming scalar, so passing a wide FromBits scalar (e.g. a SHA-512 output for Ed25519-style verification) into ScalarMul/MultiScalarMul on a fakeGLV curve crashes at compile time. Additionally, such a value is not bounded by 2p, so the 0-or-p classification would misclassify multiples of p (2p, , …) as non-zero even without the panic.
  • Narrower than the modulus: the comparison loop only checked the given prefix of p.Limbs, returning a false positive (IsZero == 1) for a non-zero element equal to the low limbs of p.

The fix forces a full modular reduction to the modulus width when the reduced element is wider than the modulus (one mulMod by one, only on this path), and compares against every modulus limb, treating missing high limbs as zero, when it is narrower.

An alternative would be to tighten Reduce's fast path itself (require len(a.Limbs) <= NbLimbs in addition to overflow == 0); that touches every consumer, so this PR keeps the change local to IsZero. Happy to rework in that direction if preferred.

Fixes #1805

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How has this been tested?

  • New TestIsZeroNonModulusWidth (Goldilocks, BN254Fr, Mod1e512) covering: 2p and as wide zero-overflow elements (≡ 0 mod p), p² + 5 (non-zero), zero, and a single-limb element equal to the low limb of p (non-zero, previously a false positive). The wide cases panic and the narrow case fails before this fix.
  • go test ./std/math/emulated/... passes locally.

How has this been benchmarked?

  • Not benchmarked — the new mulMod reduction only triggers for elements wider than the modulus, which previously panicked; the width-matched path adds no constraints. The narrow-element path adds len(p.Limbs) - len(ca.Limbs) IsZero gates only for narrow elements.

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 (not installed here; gofmt and go vet are clean)
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

🤖 Generated with Claude Code


Note

Medium Risk
Correctness fix in a core emulated-field primitive used by scalar multiplication; wrong IsZero results can break circuit soundness. Change is localized to IsZero and covered by new tests.

Overview
Fixes Field.IsZero for elements whose limb count differs from the modulus (e.g. FromBits results). Reduce's zero-overflow fast path left these unchanged, so the 0-or-p check either panicked on wide elements or returned false positives on narrow ones.

Wider than modulus: force a full mulMod reduction first (with a small-field fold that avoids native wraparound). Also short-circuits constant elements at compile time.

Narrower than modulus: compare against every modulus limb, treating missing high limbs as zero.

Adds edge-case tests covering 2p, , high-limb multiples, and single-limb prefixes of p, plus constant-wide elements.

Reviewed by Cursor Bugbot for commit 68a23d8. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread std/math/emulated/field_assert.go
0x5ea000000 and others added 2 commits August 12, 2026 12:04
IsZero assumed the element it reduces always comes back with exactly the
modulus limb count. Elements built by FromBits can legally carry a
different number of limbs with zero overflow, and Reduce's fast path
returns zero-overflow elements unchanged regardless of width:

- wider than the modulus (e.g. a 512-bit FromBits result over a 4-limb
  modulus): the limb-wise comparison against the modulus indexed out of
  range and panicked at circuit definition; the value may also be a
  multiple of p larger than 2p, which the 0-or-p check misclassifies.
  Reachable from std via scalarMulFakeGLV's IsZero(s) on any wide
  FromBits scalar.
- narrower than the modulus: the comparison checked only the given
  prefix of the modulus limbs, returning a false positive for an
  element equal to the low limbs of p.

Force a full modular reduction to the modulus width when the reduced
element is wider, and compare against every modulus limb, treating
missing high limbs as zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A constant element wider than the modulus reached the reduction added in
the previous commit, whose constant fast path folds to a constant
element with a value-dependent limb count — zero limbs when the value is
0 mod p — which the limb-indexing checks below cannot handle.

Evaluate IsZero at compile time for constant elements instead, before
any reduction. Covers the constant-wide case and avoids emitting
constraints for constants generally.

Reported by Cursor Bugbot on the PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@0x5ea000000
0x5ea000000 force-pushed the fix/emulated-iszero-non-modulus-width branch from d9ad695 to 6d8d338 Compare August 12, 2026 05:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 6d8d338. Configure here.

Comment thread std/math/emulated/field_assert.go
In small-field mode (single-limb params over a large native field, e.g.
Goldilocks over BN254), the wide-element reduction in IsZero routed
through mulMod's toSingleLimbElement, which recomposes limbs natively
and keeps the overflow at 0. For elements wider than the native field
the recomposition wraps, making IsZero answer for the wrong value; at
three limbs the understated overflow already undersizes the quotient
range check, failing honest proving.

Fold the limbs with mod-p-reduced coefficients instead before the
reduction: the single-limb result is congruent mod p, fits the native
field by the small-field-mode invariant, and declares an honest
overflow for smallMulMod's accounting.

Extend the non-modulus-width tests to 8x the modulus limb count, which
for Goldilocks (512 bits over a ~254-bit native field) exercises the
native-wrap regime; these cases fail without this fix.

Reported by Cursor Bugbot on the PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: emulated IsZero panics or returns wrong result for elements not at modulus width

1 participant