std/math/emulated/field.go:163 decides between a variable and a constant with frontend.IsCanonical. That predicate is a type switch over expr.Term and expr.LinearExpression (frontend/variable.go:18), and in the test engine every value is a *big.Int, so it is structurally always false. NewElement therefore takes the constant branch and never reaches packLimbs, enforceWidth or the limb range check.
The same file already accounts for this. At field.go:282 the sibling call site has:
// this is not a canonical variable, nor a constant. This may happen
// when some limbs are constant and some variables. Or if we are
// running in a test engine. [...]
and falls back to a bitwidth check. Line 163 has no equivalent.
Two consequences, and the second is the one that survives to production.
A multi-limb field cannot compile at all, while the test passes. With Secp256k1Fp the engine builds a one-limb element and test.IsSolved passes, but compiling the identical circuit fails:
parse circuit: enforcing width element with inexact number of limbs
emulated.(*Field[...]).enforceWidth field_assert.go:23
emulated.(*Field[...]).packLimbsWithWidth field.go:242
emulated.(*Field[...]).NewElement field.go:164
A one-limb field compiles, so the divergence ships. With Goldilocks (NbLimbs() == 1) the builder emits a 64-bit limb range check that the engine never emits. test.IsSolved passes for X = 2^100; the compiled circuit does not solve:
constraint #1 is not satisfied: 0 ⋅ 1 != 1267650600228229401496703205376
That is the shape that matters. A gadget converting a native variable into an emulated element over Goldilocks, BabyBear or KoalaBear gets zero limb range-checking under test.IsSolved, so a suite can be green on inputs the prover rejects.
The branch taken is observable from circuit code rather than by inspecting types, which matters because in the test engine everything is a *big.Int. For Secp256k1Fp the engine yields one limb where the builder needs four.
I did not find a caller of this shape inside std/, so the exposure is user code rather than the shipped gadgets. I also did not find a soundness consequence: the divergence makes the test engine more permissive, it does not make the prover accept anything it should not.
This is the same class as #650, where the test engine answered ConstantValue from an engine-wide flag rather than from the value. Both are cases where a public predicate cannot express what it is asked in the test engine. I have a repro I can turn into a regression test, and I would rather ask which direction you want before opening a PR: should IsCanonical gain a test-engine answer, or should NewElement fall back the way field.go:282 already does?
std/math/emulated/field.go:163decides between a variable and a constant withfrontend.IsCanonical. That predicate is a type switch overexpr.Termandexpr.LinearExpression(frontend/variable.go:18), and in the test engine every value is a*big.Int, so it is structurally always false.NewElementtherefore takes the constant branch and never reachespackLimbs,enforceWidthor the limb range check.The same file already accounts for this. At
field.go:282the sibling call site has:and falls back to a bitwidth check. Line 163 has no equivalent.
Two consequences, and the second is the one that survives to production.
A multi-limb field cannot compile at all, while the test passes. With
Secp256k1Fpthe engine builds a one-limb element andtest.IsSolvedpasses, but compiling the identical circuit fails:A one-limb field compiles, so the divergence ships. With
Goldilocks(NbLimbs() == 1) the builder emits a 64-bit limb range check that the engine never emits.test.IsSolvedpasses forX = 2^100; the compiled circuit does not solve:That is the shape that matters. A gadget converting a native variable into an emulated element over Goldilocks, BabyBear or KoalaBear gets zero limb range-checking under
test.IsSolved, so a suite can be green on inputs the prover rejects.The branch taken is observable from circuit code rather than by inspecting types, which matters because in the test engine everything is a
*big.Int. ForSecp256k1Fpthe engine yields one limb where the builder needs four.I did not find a caller of this shape inside
std/, so the exposure is user code rather than the shipped gadgets. I also did not find a soundness consequence: the divergence makes the test engine more permissive, it does not make the prover accept anything it should not.This is the same class as #650, where the test engine answered
ConstantValuefrom an engine-wide flag rather than from the value. Both are cases where a public predicate cannot express what it is asked in the test engine. I have a repro I can turn into a regression test, and I would rather ask which direction you want before opening a PR: shouldIsCanonicalgain a test-engine answer, or shouldNewElementfall back the wayfield.go:282already does?