Skip to content

Commit f9a9bd2

Browse files
gbotrelivokub
andauthored
feat: better emulation for small fields in large fields (#1682)
Co-authored-by: Ivo Kubjas <ivo.kubjas@consensys.net>
1 parent 64bc6af commit f9a9bd2

File tree

5 files changed

+1049
-1
lines changed

5 files changed

+1049
-1
lines changed

std/math/emulated/field.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ type Field[T FieldParams] struct {
5151
checker frontend.Rangechecker
5252

5353
deferredChecks []deferredChecker
54+
55+
// smallFieldMode indicates that the emulated field is small enough that
56+
// products fit in the native field and we can use scalar batched verification
57+
// instead of polynomial identity testing. This provides significant constraint
58+
// reduction for small field emulation (e.g., KoalaBear on BLS12-377).
59+
smallFieldMode bool
60+
smallFieldModeOnce sync.Once
5461
}
5562

5663
type ctxKey[T FieldParams] struct{}
@@ -310,3 +317,46 @@ func sum[T constraints.Ordered](a ...T) T {
310317
}
311318
return m
312319
}
320+
321+
// useSmallFieldOptimization returns true if we can use the small field
322+
// optimization for multiplication. The optimization is possible when:
323+
// - NbLimbs == 1 (emulated field fits in a single native limb)
324+
// - 2 * modBits + margin < nativeBits - 2 (products fit with margin for batching)
325+
//
326+
// When these conditions are met, we can use scalar batched verification instead
327+
// of polynomial identity testing, which significantly reduces constraint counts.
328+
func (f *Field[T]) useSmallFieldOptimization() bool {
329+
f.smallFieldModeOnce.Do(func() {
330+
// Small field optimization only works when NbLimbs == 1
331+
if f.fParams.NbLimbs() != 1 {
332+
f.smallFieldMode = false
333+
return
334+
}
335+
336+
// Small field optimization doesn't work when we're already using extension field
337+
// for multiplication checks (native field is small)
338+
if f.extensionApi != nil {
339+
f.smallFieldMode = false
340+
return
341+
}
342+
343+
// Check that products fit in the native field with margin for batching.
344+
// We need: 2 * modBits + batchingMargin < nativeBits - 2
345+
// The margin accounts for:
346+
// - γ^i scaling factors in the batched sum
347+
// - Multiple terms being summed together
348+
// We use 32 bits margin which allows for batching millions of operations.
349+
modBits := uint(f.fParams.Modulus().BitLen())
350+
nativeBits := uint(f.api.Compiler().FieldBitLen())
351+
const batchingMargin = 32
352+
353+
f.smallFieldMode = 2*modBits+batchingMargin < nativeBits-2
354+
if f.smallFieldMode {
355+
f.log.Debug().
356+
Uint("modBits", modBits).
357+
Uint("nativeBits", nativeBits).
358+
Msg("using small field optimization for emulated multiplication")
359+
}
360+
})
361+
return f.smallFieldMode
362+
}

std/math/emulated/field_mul.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ func (f *Field[T]) mulMod(a, b *Element[T], _ uint, p *Element[T]) *Element[T] {
233233
}
234234
f.enforceWidthConditional(a)
235235
f.enforceWidthConditional(b)
236+
237+
// Use small field optimization if available and no custom modulus
238+
if p == nil && f.useSmallFieldOptimization() {
239+
// For small field mode, ensure elements are single-limb
240+
// If they have more limbs due to witness initialization, convert them
241+
aLimb := f.toSingleLimbElement(a)
242+
bLimb := f.toSingleLimbElement(b)
243+
return f.smallMulMod(aLimb, bLimb)
244+
}
245+
236246
f.enforceWidthConditional(p)
237247
k, r, c, err := f.callMulHint(a, b, true, p)
238248
if err != nil {
@@ -257,9 +267,18 @@ func (f *Field[T]) checkZero(a *Element[T], p *Element[T]) {
257267
if a.isStrictZero() {
258268
return
259269
}
270+
271+
f.enforceWidthConditional(a)
272+
273+
// Use small field optimization if available and no custom modulus
274+
if p == nil && f.useSmallFieldOptimization() {
275+
aLimb := f.toSingleLimbElement(a)
276+
f.smallCheckZero(aLimb)
277+
return
278+
}
279+
260280
// the method works similarly to mulMod, but we know that we are multiplying
261281
// by one and expected result should be zero.
262-
f.enforceWidthConditional(a)
263282
f.enforceWidthConditional(p)
264283
b := f.One()
265284
k, r, c, err := f.callMulHint(a, b, false, p)

0 commit comments

Comments
 (0)