Skip to content

Commit 4598628

Browse files
authored
feat: merge-train/barretenberg (#22286)
BEGIN_COMMIT_OVERRIDE fix: minor issues in bigfield (#22216) chore: Protective asserts in proof decompression (#22253) END_COMMIT_OVERRIDE
2 parents c13d950 + efb95f9 commit 4598628

7 files changed

Lines changed: 127 additions & 30 deletions

File tree

barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ script_path="$root/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_cha
2121
# - Generate a hash for versioning: sha256sum bb-chonk-inputs.tar.gz
2222
# - Upload the compressed results: aws s3 cp bb-chonk-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-chonk-inputs-[hash(0:8)].tar.gz
2323
# Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0
24-
pinned_short_hash="aafc0a7e"
24+
pinned_short_hash="7f8e5859"
2525
pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz"
2626

2727
function update_pinned_hash_in_script {

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);

barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ template <typename Builder> inline constexpr size_t AES128_ENCRYPTION = 1559 + Z
3636
// contain only 2 of the values set for ECCVM (hence the difference of two gates between Ultra and Mega builders).
3737
template <typename Builder> inline constexpr size_t ECDSA_SECP256K1 = 42837 + ZERO_GATE;
3838
template <typename Builder>
39-
inline constexpr size_t ECDSA_SECP256R1 = 72612 + ZERO_GATE + (IsMegaBuilder<Builder> ? 2 : 0);
39+
inline constexpr size_t ECDSA_SECP256R1 = 72611 + ZERO_GATE + (IsMegaBuilder<Builder> ? 2 : 0);
4040

4141
template <typename Builder> inline constexpr size_t BLAKE2S = 2952 + ZERO_GATE + MEGA_OFFSET<Builder>;
4242
template <typename Builder> inline constexpr size_t BLAKE3 = 2158 + ZERO_GATE + MEGA_OFFSET<Builder>;
@@ -55,7 +55,7 @@ template <typename Builder> inline constexpr size_t ASSERT_EQUALITY = ZERO_GATE
5555
// Honk Recursion Constants
5656
// ========================================
5757

58-
inline constexpr size_t ROOT_ROLLUP_GATE_COUNT = 12904952;
58+
inline constexpr size_t ROOT_ROLLUP_GATE_COUNT = 12904885;
5959

6060
template <typename RecursiveFlavor>
6161
constexpr std::tuple<size_t, size_t> HONK_RECURSION_CONSTANTS(
@@ -67,18 +67,18 @@ constexpr std::tuple<size_t, size_t> HONK_RECURSION_CONSTANTS(
6767
if constexpr (std::is_same_v<RecursiveFlavor, bb::UltraRecursiveFlavor_<UltraCircuitBuilder>>) {
6868
switch (mode) {
6969
case PredicateTestCase::ConstantTrue:
70-
return std::make_tuple(681794, 0);
70+
return std::make_tuple(681762, 0);
7171
case PredicateTestCase::WitnessTrue:
7272
case PredicateTestCase::WitnessFalse:
73-
return std::make_tuple(682851, 0);
73+
return std::make_tuple(682819, 0);
7474
}
7575
} else if constexpr (std::is_same_v<RecursiveFlavor, bb::UltraZKRecursiveFlavor_<UltraCircuitBuilder>>) {
7676
switch (mode) {
7777
case PredicateTestCase::ConstantTrue:
78-
return std::make_tuple(703951, 0);
78+
return std::make_tuple(703917, 0);
7979
case PredicateTestCase::WitnessTrue:
8080
case PredicateTestCase::WitnessFalse:
81-
return std::make_tuple(705104, 0);
81+
return std::make_tuple(705070, 0);
8282
}
8383
} else if constexpr (std::is_same_v<RecursiveFlavor, bb::UltraRecursiveFlavor_<MegaCircuitBuilder>>) {
8484
switch (mode) {
@@ -100,7 +100,7 @@ constexpr std::tuple<size_t, size_t> HONK_RECURSION_CONSTANTS(
100100
if (mode != PredicateTestCase::ConstantTrue) {
101101
bb::assert_failure("Unhandled mode in MegaZKRecursiveFlavor.");
102102
}
103-
return std::make_tuple(781933, 0);
103+
return std::make_tuple(781910, 0);
104104
} else {
105105
bb::assert_failure("Unhandled recursive flavor.");
106106
}
@@ -113,7 +113,7 @@ constexpr std::tuple<size_t, size_t> HONK_RECURSION_CONSTANTS(
113113
// ========================================
114114

115115
// Gate count for Chonk recursive verification (Ultra with RollupIO)
116-
inline constexpr size_t CHONK_RECURSION_GATES = 1491513;
116+
inline constexpr size_t CHONK_RECURSION_GATES = 1491408;
117117

118118
// ========================================
119119
// Hypernova Recursion Constants
@@ -147,7 +147,7 @@ inline constexpr size_t HIDING_KERNEL_ULTRA_OPS = 127;
147147
// ========================================
148148

149149
// Gate count for ECCVM recursive verifier (Ultra-arithmetized)
150-
inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 224206;
150+
inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 224162;
151151

152152
// ========================================
153153
// Goblin AVM Recursive Verifier Constants

barretenberg/cpp/src/barretenberg/relations/non_native_field_relation.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ template <typename FF_> class NonNativeFieldRelationImpl {
9292

9393
// Bigfield Product Gate 2 (selected by q_2 * q_4):
9494
// Computes cross-term contributions in limb multiplication.
95-
// Formula: (w_1 * w_2') + (w_1' * w_2) + (w_1 * w_4 + w_2 * w_3 - w_3') * 2^68 - w_3 - w_4' = 0
95+
// Formula: (w_1 * w_2') + (w_1' * w_2) + (w_1 * w_4 + w_2 * w_3 - w_3') * 2^68 - w_4' = 0
9696
// where primed values (') denote shifted wires from the next row.
9797
auto limb_subproduct = w_1_m * w_2_shift_m + w_1_shift_m * w_2_m;
9898
auto non_native_field_gate_2_m = (w_1_m * w_4_m + w_2_m * w_3_m - w_3_shift_m);
@@ -103,7 +103,7 @@ template <typename FF_> class NonNativeFieldRelationImpl {
103103

104104
// Bigfield Product Gate 1 (selected by q_2 * q_3):
105105
// Accumulates limb products with 2^68 scaling for high-order terms.
106-
// Formula: (w_1 * w_2') + (w_1' * w_2) * 2^68 + (w_1' * w_2') - w_3 - w_4 = 0
106+
// Formula: (w_1 * w_2' + w_1' * w_2) * 2^68 + (w_1' * w_2') - w_3 - w_4 = 0
107107
limb_subproduct *= LIMB_SIZE;
108108
limb_subproduct += (w_1_shift_m * w_2_shift_m);
109109
auto non_native_field_gate_1_m = limb_subproduct;

barretenberg/cpp/src/barretenberg/stdlib/primitives/bigfield/bigfield_impl.hpp

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ template <typename Builder, typename T> bigfield<Builder, T>::bigfield(const byt
273273
const auto res = bigfield::unsafe_construct_from_limbs(limb0, limb1, limb2, limb3, true);
274274

275275
const auto num_last_limb_bits = 256 - (NUM_LIMB_BITS * 3);
276-
res.binary_basis_limbs[3].maximum_value = (uint64_t(1) << num_last_limb_bits);
276+
res.binary_basis_limbs[3].maximum_value = (uint64_t(1) << num_last_limb_bits) - 1;
277277
*this = res;
278278
set_origin_tag(bytes.get_origin_tag());
279279
}
@@ -1992,14 +1992,14 @@ void bigfield<Builder, T>::unsafe_assert_less_than(const uint256_t& upper_limit,
19921992
r2.get_witness_index(),
19931993
r3.get_witness_index(),
19941994
static_cast<size_t>(NUM_LIMB_BITS),
1995-
static_cast<size_t>(NUM_LIMB_BITS),
1995+
static_cast<size_t>(NUM_LAST_LIMB_BITS),
19961996
msg == "bigfield::unsafe_assert_less_than" ? "bigfield::unsafe_assert_less_than: r2 or r3 too large" : msg);
19971997
}
19981998

19991999
// check elements are equal mod p by proving their integer difference is a multiple of p.
20002000
// This relies on the minus operator for a-b increasing a by a multiple of p large enough so diff is non-negative
20012001
// When one of the elements is a constant and another is a witness we check equality of limbs, so if the witness
2002-
// bigfield element is in an unreduced form, it needs to be reduced first. We don't have automatice reduced form
2002+
// bigfield element is in an unreduced form, it needs to be reduced first. We don't have automatic reduced form
20032003
// detection for now, so it is up to the circuit writer to detect this
20042004
template <typename Builder, typename T>
20052005
void bigfield<Builder, T>::assert_equal(const bigfield& other, std::string const& msg) const
@@ -2010,13 +2010,16 @@ void bigfield<Builder, T>::assert_equal(const bigfield& other, std::string const
20102010
BB_ASSERT_EQ(get_value(), other.get_value(), "We expect constants to be less than the target modulus");
20112011
return;
20122012
} else if (other.is_constant()) {
2013-
// NOTE(https://github.com/AztecProtocol/barretenberg/issues/998): This can lead to a situation where
2014-
// an honest prover cannot satisfy the constraints, because `this` is not reduced, but `other` is, i.e.,
2015-
// `this` = kp + r and `other` = r
2016-
// where k is a positive integer. In such a case, the prover cannot satisfy the constraints
2017-
// because the limb-differences would not be 0 mod r. Therefore, an honest prover needs to make sure that
2018-
// `this` is reduced before calling this method. Also `other` should never be greater than the modulus by
2019-
// design. As a precaution, we assert that the circuit-constant `other` is less than the modulus.
2013+
// NOTE(https://github.com/AztecProtocol/barretenberg/issues/998): This does a limb-wise integer
2014+
// comparison, so `this` must already be in reduced form (value in [0, p)) before calling this method.
2015+
// If `this = kp + r` and `other = r`, the limbs differ and an honest prover cannot satisfy the
2016+
// constraints. Callers are responsible for calling self_reduce() first when necessary; we omit it
2017+
// here to avoid adding spurious gates in the common case where `this` is already reduced.
2018+
// `other` should never exceed the modulus by design; we assert this as a precaution.
2019+
BB_ASSERT_LT(get_value(),
2020+
modulus_u512,
2021+
"bigfield::assert_equal: 'this' is not reduced (value >= p). Call self_reduce() before comparing "
2022+
"against a constant.");
20202023
BB_ASSERT_LT(other.get_value(), modulus_u512);
20212024
field_t<Builder> t0 = (binary_basis_limbs[0].element - other.binary_basis_limbs[0].element);
20222025
field_t<Builder> t1 = (binary_basis_limbs[1].element - other.binary_basis_limbs[1].element);
@@ -2072,8 +2075,10 @@ void bigfield<Builder, T>::assert_equal(const bigfield& other, std::string const
20722075

20732076
// construct a proof that points are different mod p, when they are different mod r
20742077
// WARNING: This method doesn't have perfect completeness - for points equal mod r (or with certain difference kp
2075-
// mod r) but different mod p, you can't construct a proof. The chances of an honest prover running afoul of this
2076-
// condition are extremely small (TODO: compute probability) Note also that the number of constraints depends on how
2078+
// mod r) but different mod p, you can't construct a proof. The failure probability is at most
2079+
// (L + R + 1) / r where L = floor(a.max / p), R = floor(b.max / p), r = native field size (~2^254).
2080+
// With max bounded by 2^256 - 1 and p >= 2^249, we get L,R <= 127, so probability < 2^{-246}.
2081+
// Note also that the number of constraints depends on how
20772082
// much the values have overflown beyond p e.g. due to an addition chain The function is based on the following.
20782083
// Suppose a-b = 0 mod p. Then a-b = k*p for k in a range [-R,L] for largest L and R such that L*p>= a, R*p>=b.
20792084
// And also a-b = k*p mod r for such k. Thus we can verify a-b is non-zero mod p by taking the product of such values
@@ -2147,7 +2152,7 @@ template <typename Builder, typename T> void bigfield<Builder, T>::self_reduce()
21472152

21482153
BB_ASSERT_LT((uint1024_t(1) << maximum_quotient_bits) * uint1024_t(modulus_u512) + DEFAULT_MAXIMUM_REMAINDER,
21492154
get_maximum_crt_product());
2150-
quotient.binary_basis_limbs[0] = Limb(quotient_limb, uint256_t(1) << maximum_quotient_bits);
2155+
quotient.binary_basis_limbs[0] = Limb(quotient_limb, (uint256_t(1) << maximum_quotient_bits) - 1);
21512156
quotient.binary_basis_limbs[1] = Limb(field_t<Builder>::from_witness_index(context, context->zero_idx()), 0);
21522157
quotient.binary_basis_limbs[2] = Limb(field_t<Builder>::from_witness_index(context, context->zero_idx()), 0);
21532158
quotient.binary_basis_limbs[3] = Limb(field_t<Builder>::from_witness_index(context, context->zero_idx()), 0);

barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ TYPED_TEST(stdlib_field_conversion, GateCountScalarDeserialization)
356356
TYPED_TEST(stdlib_field_conversion, GateCountBigfieldDeserialization)
357357
{
358358
// Deserializing a single bigfield element is expensive due to creating new ranges for range constraints
359-
this->template check_deserialization_gate_count<fq<TypeParam>>([] { return bb::fq::random_element(); }, 3515);
359+
this->template check_deserialization_gate_count<fq<TypeParam>>([] { return bb::fq::random_element(); }, 3513);
360360
}
361361

362362
/**
@@ -365,7 +365,7 @@ TYPED_TEST(stdlib_field_conversion, GateCountBigfieldDeserialization)
365365
*/
366366
TYPED_TEST(stdlib_field_conversion, GateCountMultipleBigfieldDeserialization)
367367
{
368-
this->template check_deserialization_gate_count<fq<TypeParam>>([] { return bb::fq::random_element(); }, 3914, 10);
368+
this->template check_deserialization_gate_count<fq<TypeParam>>([] { return bb::fq::random_element(); }, 3913, 10);
369369
}
370370

371371
/**
@@ -389,7 +389,7 @@ TYPED_TEST(stdlib_field_conversion, GateCountMultipleBN254PointDeserialization)
389389
{
390390
using Builder = TypeParam;
391391

392-
constexpr uint32_t expected = std::is_same_v<Builder, bb::UltraCircuitBuilder> ? 5751 : 0;
392+
constexpr uint32_t expected = std::is_same_v<Builder, bb::UltraCircuitBuilder> ? 5746 : 0;
393393
this->template check_deserialization_gate_count<bn254_element<Builder>>(
394394
[] { return curve::BN254::AffineElement::random_element(); }, expected, 10);
395395
}

0 commit comments

Comments
 (0)