Skip to content

Commit efb95f9

Browse files
authored
chore: Protective asserts in proof decompression (#22253)
Resolves the following defense in depth issues related to Chonk Proof decompression: - [2205](AztecProtocol/barretenberg-claude#2205): ensure deserialized u256s are less than field modulus. (No real risk in not fixing this; there's no attack) - EDIT: [2207](AztecProtocol/barretenberg-claude#2207): is a non-issue because (0, sqrt(3)) is NOT on BN. keeping the original x==0 representaiton for infinity is fine for both BN and Grump
1 parent 21c761f commit efb95f9

2 files changed

Lines changed: 94 additions & 2 deletions

File tree

barretenberg/cpp/src/barretenberg/chonk/chonk.test.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,3 +591,84 @@ TEST_F(ChonkTests, ProofCompressionRoundtrip)
591591
// Verify the decompressed proof
592592
EXPECT_TRUE(verify_chonk(decompressed, vk_and_hash));
593593
}
594+
595+
/**
596+
* @brief Build a valid compressed proof, then corrupt a specific 32-byte element.
597+
*/
598+
static std::vector<uint8_t> compress_and_corrupt(const ChonkProof& proof, size_t element_idx, const uint256_t& value)
599+
{
600+
auto compressed = ProofCompressor::compress_chonk_proof(proof);
601+
size_t byte_offset = element_idx * 32;
602+
EXPECT_LT(byte_offset + 32, compressed.size());
603+
// Overwrite the element with the given value (big-endian)
604+
std::vector<uint8_t> buf;
605+
write(buf, value);
606+
std::copy(buf.begin(), buf.end(), compressed.begin() + static_cast<ptrdiff_t>(byte_offset));
607+
return compressed;
608+
}
609+
610+
// Rejects a BN scalar value >= Fr::modulus
611+
TEST_F(ChonkTests, DecompressionRejectsNonCanonicalBN254Scalar)
612+
{
613+
TestSettings settings{ .log2_num_gates = SMALL_LOG_2_NUM_GATES };
614+
auto [proof, vk_and_hash] = accumulate_and_prove_ivc(/*num_app_circuits=*/1, settings);
615+
size_t mega_num_pub_inputs =
616+
proof.hiding_oink_proof.size() - ProofLength::Oink<MegaZKFlavor>::LENGTH_WITHOUT_PUB_INPUTS;
617+
ASSERT_GT(mega_num_pub_inputs, 0); // Need at least one public input (BN254 scalar)
618+
619+
// Element 0 is the first public input (a BN254 scalar). Set it to Fr::modulus (non-canonical).
620+
using Fr = curve::BN254::ScalarField;
621+
622+
auto corrupted = compress_and_corrupt(proof, 0, Fr::modulus);
623+
EXPECT_THROW_OR_ABORT(ProofCompressor::decompress_chonk_proof(corrupted, mega_num_pub_inputs), "");
624+
}
625+
626+
// Rejects a BN commitment x-coordinate >= Fq::modulus
627+
TEST_F(ChonkTests, DecompressionRejectsNonCanonicalBN254CommitmentX)
628+
{
629+
using Fq = curve::BN254::BaseField;
630+
631+
TestSettings settings{ .log2_num_gates = SMALL_LOG_2_NUM_GATES };
632+
auto [proof, vk_and_hash] = accumulate_and_prove_ivc(/*num_app_circuits=*/1, settings);
633+
size_t mega_num_pub_inputs =
634+
proof.hiding_oink_proof.size() - ProofLength::Oink<MegaZKFlavor>::LENGTH_WITHOUT_PUB_INPUTS;
635+
636+
// First BN254 commitment is at element index mega_num_pub_inputs.
637+
// Set x-coordinate to Fq::modulus + 1 (non-canonical, with no sign bit).
638+
auto corrupted = compress_and_corrupt(proof, mega_num_pub_inputs, Fq::modulus + 1);
639+
EXPECT_THROW_OR_ABORT(ProofCompressor::decompress_chonk_proof(corrupted, mega_num_pub_inputs), "");
640+
}
641+
642+
// Rejects a canonical x-coordinate that is not on the BN254 curve
643+
TEST_F(ChonkTests, DecompressionRejectsInvalidBN254CurvePoint)
644+
{
645+
using Fq = curve::BN254::BaseField;
646+
647+
TestSettings settings{ .log2_num_gates = SMALL_LOG_2_NUM_GATES };
648+
auto [proof, vk_and_hash] = accumulate_and_prove_ivc(/*num_app_circuits=*/1, settings);
649+
size_t mega_num_pub_inputs =
650+
proof.hiding_oink_proof.size() - ProofLength::Oink<MegaZKFlavor>::LENGTH_WITHOUT_PUB_INPUTS;
651+
652+
// x=4 is not on BN254
653+
Fq x_not_on_curve(4);
654+
Fq rhs = x_not_on_curve * x_not_on_curve * x_not_on_curve + Fq(3);
655+
auto [is_sq, _] = rhs.sqrt();
656+
ASSERT_FALSE(is_sq);
657+
658+
auto corrupted = compress_and_corrupt(proof, mega_num_pub_inputs, uint256_t(x_not_on_curve));
659+
EXPECT_THROW_OR_ABORT(ProofCompressor::decompress_chonk_proof(corrupted, mega_num_pub_inputs), "");
660+
}
661+
662+
// Rejects a compressed proof that is too short (read_u256 bounds check)
663+
TEST_F(ChonkTests, DecompressionRejectsTruncatedProof)
664+
{
665+
TestSettings settings{ .log2_num_gates = SMALL_LOG_2_NUM_GATES };
666+
auto [proof, vk_and_hash] = accumulate_and_prove_ivc(/*num_app_circuits=*/1, settings);
667+
size_t mega_num_pub_inputs =
668+
proof.hiding_oink_proof.size() - ProofLength::Oink<MegaZKFlavor>::LENGTH_WITHOUT_PUB_INPUTS;
669+
670+
auto compressed = ProofCompressor::compress_chonk_proof(proof);
671+
// Truncate by removing the last 32-byte element
672+
compressed.resize(compressed.size() - 32);
673+
EXPECT_THROW_OR_ABORT(ProofCompressor::decompress_chonk_proof(compressed, mega_num_pub_inputs), "");
674+
}

barretenberg/cpp/src/barretenberg/chonk/proof_compression.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class ProofCompressor {
5959

6060
static uint256_t read_u256(const std::vector<uint8_t>& data, size_t& pos)
6161
{
62+
BB_ASSERT(pos + 32 <= data.size());
6263
uint256_t val{ 0, 0, 0, 0 };
6364
for (int i = 31; i >= 0; --i) {
6465
val.data[i / 8] |= static_cast<uint64_t>(data[pos++]) << (8 * (i % 8));
@@ -502,20 +503,27 @@ class ProofCompressor {
502503
size_t pos = 0;
503504

504505
// BN254 callbacks
505-
auto bn254_scalar = [&]() { flat.emplace_back(read_u256(compressed, pos)); };
506+
auto bn254_scalar = [&]() {
507+
uint256_t raw = read_u256(compressed, pos);
508+
BB_ASSERT(raw < Fr::modulus);
509+
flat.emplace_back(raw);
510+
};
506511

507512
auto bn254_comm = [&]() {
508513
uint256_t raw = read_u256(compressed, pos);
509514
bool sign = (raw & SIGN_BIT_MASK) != 0;
510515
uint256_t x_val = raw & ~SIGN_BIT_MASK;
511516

517+
// Point-at-infinity is encoded as all zeros (x=0, sign=false).
518+
// Unambiguous because x=0 is not on BN254
512519
if (x_val == uint256_t(0) && !sign) {
513520
for (int j = 0; j < 4; j++) {
514521
flat.emplace_back(Fr::zero());
515522
}
516523
return;
517524
}
518525

526+
BB_ASSERT(x_val < Fq::modulus);
519527
Fq x(x_val);
520528
Fq y_squared = x * x * x + Bn254G1Params::b;
521529
auto [is_square, y] = y_squared.sqrt();
@@ -539,14 +547,16 @@ class ProofCompressor {
539547
bool sign = (raw & SIGN_BIT_MASK) != 0;
540548
uint256_t x_val = raw & ~SIGN_BIT_MASK;
541549

550+
// Point-at-infinity is encoded as all zeros (x=0, sign=false).
551+
// Unambiguous because x=0 is not on Grumpkin
542552
if (x_val == uint256_t(0) && !sign) {
543553
flat.emplace_back(Fr::zero());
544554
flat.emplace_back(Fr::zero());
545555
return;
546556
}
547557

558+
BB_ASSERT(x_val < Fr::modulus);
548559
Fr x(x_val);
549-
// Grumpkin curve: y² = x³ + b, where b = -17 (in BN254::ScalarField)
550560
Fr y_squared = x * x * x + grumpkin::G1Params::b;
551561
auto [is_square, y] = y_squared.sqrt();
552562
BB_ASSERT(is_square);
@@ -561,6 +571,7 @@ class ProofCompressor {
561571

562572
auto grumpkin_scalar = [&]() {
563573
uint256_t raw = read_u256(compressed, pos);
574+
BB_ASSERT(raw < Fq::modulus);
564575
Fq fq_val(raw);
565576
auto [lo, hi] = split_fq(fq_val);
566577
flat.emplace_back(lo);

0 commit comments

Comments
 (0)