Skip to content

Commit b19a5b5

Browse files
authored
chore: add a shplemini failure test (#23147)
.
1 parent 1b0b06c commit b19a5b5

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

barretenberg/cpp/src/barretenberg/commitment_schemes/claim_batcher.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ template <typename Curve> struct ClaimBatcher_ {
7878
}
7979
if (shifted) {
8080
// r⁻¹ ⋅ (1/(z−r) − ν/(z+r))
81+
//
82+
// This scalar is the verifier-side to-be-shifted-by-one PCS contract: every commitment in
83+
// `shifted.commitments` is required to be a commitment to a polynomial with constant term zero.
84+
// A commitment to a polynomial with poly[0] != 0 opens to G(r)/r = poly[0]/r + G_shift(r) on
85+
// the commitment side, whereas the claimed MLE evaluation poly_shift(u) reconstructs to
86+
// G_shift(r) at the Gemini challenge. The two sides differ by poly[0]/r, the Shplonk quotient
87+
// is then not a polynomial, and the KZG pairing check rejects with overwhelming probability
88+
// over the FS challenges.
89+
// Regression: commitment_schemes/shplonk/shplemini.test.cpp::ToBeShiftedNonZeroConstantTermRejected.
8190
shifted->scalar =
8291
r_challenge.invert() * (inverse_vanishing_eval_pos - nu_challenge * inverse_vanishing_eval_neg);
8392
}

barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplemini.test.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,114 @@ TYPED_TEST(ShpleminiTest, HighDegreeAttackReject)
610610
}
611611
}
612612

613+
/**
614+
* @brief Soundness of the to-be-shifted PCS backstop: a commitment with a non-zero constant
615+
* coefficient must be rejected.
616+
*
617+
* @details For "to-be-shifted-by-one" polynomials (e.g. z_perm in Honk flavors), the verifier
618+
* batches the commitment com(p) with scalar r^{-1} against the claimed MLE evaluation
619+
* p_shift(u). With G(X) the univariate whose coefficients are p in Lagrange basis, the
620+
* commitment side opens to G(r)/r = p[0]/r + G_shift(r), while the MLE side delivers
621+
* G_shift(r) via the Gemini fold. The two sides differ by the algebraic term p[0]/r; when
622+
* p[0] != 0 the would-be Shplonk quotient is a rational function rather than a polynomial,
623+
* and the KZG pairing check rejects with overwhelming probability over the FS challenges.
624+
*
625+
* This is the implicit PCS-shift backstop that several relations rely on to enforce
626+
* z_perm[0] = 0 in disabled rows of ZK flavors (where row-disabling zeros the explicit
627+
* lagrange_first * z_perm subrelation). The test commits to (p + c * delta_0), claims the
628+
* shifted MLE evaluation of the honest p (which is unchanged by adding c at index 0, since
629+
* shifting drops the constant term), and confirms the verifier rejects.
630+
*/
631+
TYPED_TEST(ShpleminiTest, ToBeShiftedNonZeroConstantTermRejected)
632+
{
633+
using Curve = typename TypeParam::Curve;
634+
using Fr = typename Curve::ScalarField;
635+
using GroupElement = typename Curve::Element;
636+
using Commitment = typename Curve::AffineElement;
637+
using CK = typename TypeParam::CommitmentKey;
638+
using ShpleminiProver = ShpleminiProver_<Curve>;
639+
using ShpleminiVerifier = ShpleminiVerifier_<Curve>;
640+
641+
CK ck = create_commitment_key<CK>(this->n);
642+
643+
auto mle_opening_point = this->random_evaluation_point(this->log_n);
644+
645+
MockClaimGenerator<Curve> mock_claims(this->n,
646+
/*num_polynomials*/ this->num_polynomials,
647+
/*num_to_be_shifted*/ this->num_shiftable,
648+
mle_opening_point,
649+
ck);
650+
651+
auto prover_transcript = NativeTranscript::test_prover_init_empty();
652+
653+
const auto opening_claim =
654+
ShpleminiProver::prove(this->n, mock_claims.polynomial_batcher, mle_opening_point, ck, prover_transcript);
655+
656+
// For KZG, run the opening proof now: KZG never binds the claim into Fiat-Shamir, so the
657+
// verifier can be handed a tampered claim later without affecting the prover transcript.
658+
// For IPA, defer the opening proof until after the tampered batched claim is available; the
659+
// adversarial prover hashes that claim into its FS buffer to match the verifier (see below).
660+
if constexpr (!std::is_same_v<TypeParam, GrumpkinSettings>) {
661+
KZG<Curve>::compute_opening_proof(ck, opening_claim, prover_transcript);
662+
}
663+
664+
// Simulate adversary: replace the first to-be-shifted commitment with com(p + c * delta_0),
665+
// i.e. add c * [1]_1 to it. The shifted MLE evaluation is unchanged (shifting drops the [0]
666+
// coefficient). The unshifted MLE evaluation of p + c * delta_0 differs from the unshifted MLE
667+
// evaluation of p by c * prod_i (1 - u_i); update the unshifted counterpart claim accordingly
668+
// so that any rejection cannot be attributed to a stale unshifted-side mismatch.
669+
const Fr c = Fr::random_element();
670+
const Commitment g1_identity = this->vk().get_g1_identity();
671+
const auto tampered = Commitment(GroupElement(mock_claims.to_be_shifted.commitments[0]) + g1_identity * c);
672+
673+
Fr lagrange0_at_u = Fr(1);
674+
for (const auto& u_i : mle_opening_point) {
675+
lagrange0_at_u *= (Fr(1) - u_i);
676+
}
677+
const size_t unshifted_idx = this->num_polynomials - this->num_shiftable; // first to-be-shifted in unshifted batch
678+
mock_claims.to_be_shifted.commitments[0] = tampered;
679+
mock_claims.unshifted.commitments[unshifted_idx] = tampered;
680+
mock_claims.unshifted.evals[unshifted_idx] += c * lagrange0_at_u;
681+
682+
auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript);
683+
684+
auto batch_opening_claim = ShpleminiVerifier::compute_batch_opening_claim(
685+
mock_claims.claim_batcher, mle_opening_point, g1_identity, verifier_transcript)
686+
.batch_opening_claim;
687+
688+
if constexpr (std::is_same_v<TypeParam, GrumpkinSettings>) {
689+
// Adversarial IPA prover: stage the prover transcript with the same reduced claim the
690+
// verifier will hash via add_claim_to_hash_buffer, then fold the honest polynomial. This
691+
// keeps prover/verifier FS in sync so the rejection isolates the inner-product relation
692+
// rather than transcript divergence.
693+
const auto reduced = TestFixture::IPA::reduce_batch_opening_claim(batch_opening_claim);
694+
prover_transcript->add_to_hash_buffer("IPA:commitment", reduced.commitment);
695+
prover_transcript->add_to_hash_buffer("IPA:challenge", reduced.opening_pair.challenge);
696+
prover_transcript->add_to_hash_buffer("IPA:evaluation", reduced.opening_pair.evaluation);
697+
TestFixture::IPA::compute_opening_proof_internal(ck, opening_claim, prover_transcript);
698+
699+
// The verifier transcript was initialized before the IPA prover wrote its bytes; refresh
700+
// its view of proof_data so the IPA verifier can read them.
701+
verifier_transcript->test_get_proof_data() = prover_transcript->test_get_proof_data();
702+
703+
auto result =
704+
TestFixture::IPA::reduce_verify_batch_opening_claim(batch_opening_claim, this->vk(), verifier_transcript);
705+
EXPECT_EQ(result, false);
706+
} else {
707+
const auto pairing_points =
708+
KZG<Curve>::reduce_verify_batch_opening_claim(std::move(batch_opening_claim), verifier_transcript);
709+
EXPECT_EQ(pairing_points.check(), false);
710+
}
711+
712+
// Confirm the rejection is not an artifact of transcript divergence: a fresh challenge with
713+
// the same label drawn from both transcripts must agree. For KZG this is automatic (the
714+
// claim is never hashed into FS). For IPA we matched the prover and verifier hash buffers
715+
// explicitly above; if this check fails, the rejection above could be attributed to the
716+
// prover and verifier consuming different challenges rather than the PCS check itself.
717+
EXPECT_EQ(prover_transcript->template get_challenge<Fr>("transcript_sync_check"),
718+
verifier_transcript->template get_challenge<Fr>("transcript_sync_check"));
719+
}
720+
613721
/**
614722
* @brief Test that consistency_checked is false when a Libra univariate evaluation is corrupted.
615723
* @details This test simulates a malicious prover sending a corrupted Libra evaluation via the

0 commit comments

Comments
 (0)