@@ -341,7 +341,9 @@ struct MsmScalar {
341341 }
342342};
343343
344- template <typename Builder> class MultiScalarMulInfinityTests : public ::testing::Test {
344+ // Shared single-term MSM circuit helpers: build a one point/one scalar MSM constraint with predicate=1
345+ // from explicit witness values, and run the resulting circuit.
346+ template <typename Builder> class MsmSingleTermFixture : public ::testing::Test {
345347 protected:
346348 static void SetUpTestSuite () { bb::srs::init_file_crs_factory (bb::srs::bb_crs_path ()); }
347349
@@ -402,6 +404,8 @@ template <typename Builder> class MultiScalarMulInfinityTests : public ::testing
402404 }
403405};
404406
407+ template <typename Builder> class MultiScalarMulInfinityTests : public MsmSingleTermFixture <Builder> {};
408+
405409TYPED_TEST_SUITE (MultiScalarMulInfinityTests, BuilderTypes);
406410
407411// scalar=0 → result = ∞: valid proof with out_point_is_infinite=1.
@@ -449,3 +453,135 @@ TYPED_TEST(MultiScalarMulInfinityTests, ForgedFiniteFlagOnInfinityResultFails)
449453 EXPECT_TRUE (!ok || err.find (" assert_eq" ) != std::string::npos)
450454 << " Forged finite flag on infinity result should fail" ;
451455}
456+
457+ // ============================================================
458+ // Scalar field-bounds tests
459+ // ============================================================
460+ //
461+ // The MSM opcode receives a Grumpkin scalar as two field limbs: lo (low 128 bits) and hi (next 126
462+ // bits), reconstructing v = lo + hi * 2^128. cycle_scalar's public constructor adds an in-circuit
463+ // check that v < r, where r == bb::fq::modulus is the Grumpkin scalar field modulus (and also the
464+ // order of the Grumpkin group, since Grumpkin's scalar field is BN254's base field). batch_mul
465+ // additionally range-constrains the limbs to lo < 2^128 and hi < 2^126. These tests pin behaviour
466+ // at and beyond the modulus boundary: an out-of-range scalar must make the circuit unsatisfiable,
467+ // and the group law's s ≡ s + r equivalence must not let a caller smuggle a non-canonical scalar
468+ // through to barretenberg.
469+
470+ namespace {
471+ // r = order of the Grumpkin group = bb::fq::modulus.
472+ const uint256_t grumpkin_scalar_modulus = bb::fq::modulus;
473+
474+ // Build an MsmScalar straight from a uint256_t value, splitting at the 128-bit limb boundary with no
475+ // modular reduction (so out-of-field values can be expressed).
476+ MsmScalar msm_scalar_from_u256 (const uint256_t & v)
477+ {
478+ return { MsmFF (v.slice (0 , 128 )), MsmFF (v.slice (128 , 256 )) };
479+ }
480+ } // namespace
481+
482+ template <typename Builder> class MultiScalarMulScalarBoundsTests : public MsmSingleTermFixture <Builder> {};
483+
484+ TYPED_TEST_SUITE (MultiScalarMulScalarBoundsTests, BuilderTypes);
485+
486+ // scalar == r: rejected. The in-circuit "scalar < r" check fails. (r·P = O, so the caller gains
487+ // nothing by claiming the point at infinity as the result.)
488+ TYPED_TEST (MultiScalarMulScalarBoundsTests, ScalarEqualToModulusFails)
489+ {
490+ BB_DISABLE_ASSERTS ();
491+ MsmGrumpkinPoint point = MsmGrumpkinPoint::random_element ();
492+ auto [constraint, witness] = TestFixture::make_msm (
493+ MsmAcirPoint::from_native (point), msm_scalar_from_u256 (grumpkin_scalar_modulus), MsmAcirPoint::infinity ());
494+
495+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
496+ EXPECT_FALSE (ok) << " scalar == Grumpkin scalar modulus must not produce a satisfiable circuit" ;
497+ }
498+
499+ // scalar == r + 1: rejected, even though (r + 1)·P == 1·P == P. The in-circuit "scalar < r" check
500+ // fails despite both limbs being within their range constraints.
501+ TYPED_TEST (MultiScalarMulScalarBoundsTests, ScalarModulusPlusOneFails)
502+ {
503+ BB_DISABLE_ASSERTS ();
504+ MsmGrumpkinPoint point = MsmGrumpkinPoint::random_element ();
505+ auto [constraint, witness] = TestFixture::make_msm (MsmAcirPoint::from_native (point),
506+ msm_scalar_from_u256 (grumpkin_scalar_modulus + uint256_t (1 )),
507+ MsmAcirPoint::from_native (point));
508+
509+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
510+ EXPECT_FALSE (ok) << " scalar == Grumpkin scalar modulus + 1 must not produce a satisfiable circuit" ;
511+ }
512+
513+ // scalar == r - 1: the largest in-field scalar. This must prove fine.
514+ TYPED_TEST (MultiScalarMulScalarBoundsTests, ScalarModulusMinusOneProves)
515+ {
516+ BB_DISABLE_ASSERTS ();
517+ MsmGrumpkinPoint point = MsmGrumpkinPoint::random_element ();
518+ bb::fq scalar_native = bb::fq (grumpkin_scalar_modulus - uint256_t (1 ));
519+ MsmGrumpkinPoint result = point * scalar_native;
520+ ASSERT_FALSE (result.is_point_at_infinity ());
521+ auto [constraint, witness] = TestFixture::make_msm (MsmAcirPoint::from_native (point),
522+ msm_scalar_from_u256 (grumpkin_scalar_modulus - uint256_t (1 )),
523+ MsmAcirPoint::from_native (result));
524+
525+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
526+ EXPECT_TRUE (ok) << " scalar == Grumpkin scalar modulus - 1 (largest in-field scalar) should prove. err: " << err;
527+ }
528+
529+ // scalar == 2^254 - 1: the largest value the (128 + 126)-bit limb encoding can represent. Both limbs
530+ // satisfy their range constraints, so the only thing rejecting it is the in-circuit "scalar < r"
531+ // check — exercising the "limbs in range but value out of field" path.
532+ TYPED_TEST (MultiScalarMulScalarBoundsTests, MaxRepresentableScalarFails)
533+ {
534+ BB_DISABLE_ASSERTS ();
535+ MsmGrumpkinPoint point = MsmGrumpkinPoint::random_element ();
536+ uint256_t max_representable = (uint256_t (1 ) << 254 ) - uint256_t (1 );
537+ MsmGrumpkinPoint result = point * bb::fq (max_representable); // (2^254 - 1) mod r
538+ auto [constraint, witness] = TestFixture::make_msm (
539+ MsmAcirPoint::from_native (point), msm_scalar_from_u256 (max_representable), MsmAcirPoint::from_native (result));
540+
541+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
542+ EXPECT_FALSE (ok) << " scalar == 2^254 - 1 (> Grumpkin modulus) must not produce a satisfiable circuit" ;
543+ }
544+
545+ // hi limb == 2^126 (one bit too wide), lo == 0, i.e. scalar value 2^254. Both the limb range
546+ // constraint (hi < 2^126) and the "scalar < r" check reject it.
547+ TYPED_TEST (MultiScalarMulScalarBoundsTests, ScalarWithOversizedHiLimbFails)
548+ {
549+ BB_DISABLE_ASSERTS ();
550+ MsmGrumpkinPoint point = MsmGrumpkinPoint::random_element ();
551+ uint256_t two_pow_254 = uint256_t (1 ) << 254 ;
552+ MsmGrumpkinPoint result = point * bb::fq (two_pow_254);
553+ MsmScalar scalar{ MsmFF (0 ), MsmFF (uint256_t (1 ) << 126 ) }; // hi has 127 bits
554+ auto [constraint, witness] =
555+ TestFixture::make_msm (MsmAcirPoint::from_native (point), scalar, MsmAcirPoint::from_native (result));
556+
557+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
558+ EXPECT_FALSE (ok) << " scalar hi limb of 127 bits (value 2^254) must not produce a satisfiable circuit" ;
559+ }
560+
561+ // Group-law equivalence does not transfer: s·P == (s + r)·P, but the circuit accepts only the
562+ // canonical scalar s. Adding the Grumpkin modulus to a scalar cannot reprove the same output.
563+ TYPED_TEST (MultiScalarMulScalarBoundsTests, AddingGrumpkinModulusDoesNotReproveSameOutput)
564+ {
565+ BB_DISABLE_ASSERTS ();
566+ MsmGrumpkinPoint point = MsmGrumpkinPoint::random_element ();
567+ bb::fq scalar_native = bb::fq (5 );
568+ MsmGrumpkinPoint result = point * scalar_native;
569+ ASSERT_FALSE (result.is_point_at_infinity ());
570+
571+ // Sanity: the canonical scalar proves the result.
572+ {
573+ auto [constraint, witness] = TestFixture::make_msm (
574+ MsmAcirPoint::from_native (point), MsmScalar::from_native (scalar_native), MsmAcirPoint::from_native (result));
575+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
576+ EXPECT_TRUE (ok) << " canonical scalar should prove the MSM result. err: " << err;
577+ }
578+
579+ // The non-canonical scalar s + r yields the same point mathematically, but the circuit rejects it.
580+ {
581+ auto [constraint, witness] = TestFixture::make_msm (MsmAcirPoint::from_native (point),
582+ msm_scalar_from_u256 (uint256_t (5 ) + grumpkin_scalar_modulus),
583+ MsmAcirPoint::from_native (result));
584+ auto [ok, err] = TestFixture::run_circuit (constraint, witness);
585+ EXPECT_FALSE (ok) << " scalar s + r must not reprove the output of scalar s" ;
586+ }
587+ }
0 commit comments