diff --git a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh index 401acf035a3e..dec04ce11877 100755 --- a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh +++ b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh @@ -13,7 +13,7 @@ cd .. # - Generate a hash for versioning: sha256sum bb-chonk-inputs.tar.gz # - 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 # Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0 -pinned_short_hash="9768ca58" +pinned_short_hash="2b9b79ac" pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz" script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")/scripts" && pwd)/$(basename "${BASH_SOURCE[0]}")" @@ -85,13 +85,13 @@ function prove_and_verify_inputs { echo "Running proof test for $1..." $bb prove --scheme chonk --ivc_inputs_path "$flow_folder/ivc-inputs.msgpack" > /dev/null 2>&1 || prove_exit_code=$? - if [[ $proof_exit_code -ne 0 ]]; then + # if [[ $proof_exit_code -ne 0 ]]; then echo "Proof test failed for flow $1. Please re-run the script with flag --update_inputs." cp "$flow_folder/ivc-inputs.msgpack" "$root/yarn-project/end-to-end/example-app-ivc-inputs-out/$1/ivc-inputs.msgpack" echo "Inputs copied in yarn-project for debugging" exit 1 - fi + # fi } export -f prove_and_verify_inputs @@ -130,6 +130,7 @@ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then none Test that Chonk standalone VKs haven't changed --update_inputs Generate new IVC inputs and upload to S3 --prove_and_verify Prove and verify current pinned inputs + --download_pinned_inputs Download pinned inputs to yarn-project for local debugging -h, --help Show this help message Description: @@ -163,6 +164,30 @@ elif [[ "${1:-}" == "--update_inputs" ]]; then echo "Inputs successfully updated." exit 0 +elif [[ "${1:-}" == "--download_pinned_inputs" ]]; then + # Download pinned inputs to yarn-project for local debugging + set -eu + local_output_dir="$root/yarn-project/end-to-end/example-app-ivc-inputs-out" + + echo "Downloading pinned IVC inputs (hash: $pinned_short_hash) to $local_output_dir..." + + mkdir -p "$local_output_dir" + cd "$local_output_dir" + + # Clean existing contents + rm -rf ./* + + if ! curl -s -f "$pinned_chonk_inputs_url" -o bb-chonk-inputs.tar.gz; then + echo "Error: Failed to download pinned IVC inputs from $pinned_chonk_inputs_url" + exit 1 + fi + + tar -xzf bb-chonk-inputs.tar.gz -C . + rm -f bb-chonk-inputs.tar.gz + + echo "Done. Inputs downloaded to: $local_output_dir" + ls -la "$local_output_dir" + exit 0 else export inputs_dir=$(mktemp -d) trap 'rm -rf "$inputs_dir" bb-chonk-inputs.tar.gz' EXIT SIGINT @@ -204,7 +229,7 @@ else echo "No VK changes detected. Short hash is: ${pinned_short_hash}" elif [[ $exit_code -eq 1 ]]; then # All flows had VK changes - echo "VK changes detected. Please re-run the script with --update_fast or --update_inputs" + echo "VK changes detected. Please re-run the script with --update_inputs" exit 1 else # At least one real error diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.cpp index 7f97ad2642a1..adbd9e77c2eb 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.cpp @@ -1,5 +1,5 @@ // === AUDIT STATUS === -// internal: { status: Planned, auditors: [], commit: } +// internal: { status: Complete, auditors: [Khashayar], commit: } // external_1: { status: not started, auditors: [], commit: } // external_2: { status: not started, auditors: [], commit: } // ===================== @@ -19,7 +19,6 @@ template void create_aes128_constraints(Builder& builder, con { using field_ct = bb::stdlib::field_t; - // Packs 16 bytes from the inputs (plaintext, iv, key) into a field element const auto convert_input = [&](std::span, std::dynamic_extent> inputs, size_t padding, Builder& builder) { @@ -27,6 +26,11 @@ template void create_aes128_constraints(Builder& builder, con for (size_t i = 0; i < 16 - padding; ++i) { converted *= 256; field_ct byte = to_field_ct(inputs[i], builder); + // Noir enforces bytes to be in the range [0, 255] by type declarations, however, if inputs are taken + // from + // ACIR directly, these ranges should be enforced by the range constraint. In case these range + // constraints already exist we won't be paying for the extra constraint. + byte.create_range_constraint(8); converted += byte; } for (size_t i = 0; i < padding; ++i) { @@ -43,6 +47,10 @@ template void create_aes128_constraints(Builder& builder, con for (const auto& output : outputs) { converted *= 256; field_ct byte = field_ct::from_witness_index(&builder, output); + // Noir enforces bytes to be in the range [0, 255] by type declarations, however, if inputs are taken from + // ACIR directly, these ranges should be enforced by the range constraint. In case these range constraints + // already exist we won't be paying for the extra constraint. + byte.create_range_constraint(8); converted += byte; } return converted; diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.test.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.test.cpp index eeed5141b4a1..d55553b2b094 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.test.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/aes128_constraint.test.cpp @@ -467,3 +467,308 @@ TEST(AES128PaddingBug, BlockAlignedInputMissingPaddingBlock) bool circuit_valid = CircuitChecker::check(builder); EXPECT_TRUE(circuit_valid) << "Circuit should pass with buggy 32-byte output"; } + +// ============================================================================= +// Range Constraint Regression Tests +// ============================================================================= +// These tests verify that the AES128 constraint properly enforces that all byte +// values (input, key, IV, output) are in the range [0, 255]. Values outside this +// range should cause circuit verification to fail. +// ============================================================================= + +class AES128RangeConstraintTest : public ::testing::Test { + protected: + static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); } + + using Builder = UltraCircuitBuilder; + using FF = Builder::FF; + + // Valid test vectors for AES-128-CBC (16 bytes = 1 block) + static constexpr std::array valid_plaintext = { 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, + 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a }; + + static constexpr std::array valid_key = { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, + 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c }; + + static constexpr std::array valid_iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; + + /** + * @brief Compute valid ciphertext for the test vectors. + */ + static std::array compute_ciphertext() + { + std::vector buffer(16); + std::array key_bytes{}; + std::array iv_bytes{}; + + for (size_t i = 0; i < 16; ++i) { + buffer[i] = static_cast(uint256_t(valid_plaintext[i])); + key_bytes[i] = static_cast(uint256_t(valid_key[i])); + iv_bytes[i] = static_cast(uint256_t(valid_iv[i])); + } + + crypto::aes128_encrypt_buffer_cbc(buffer.data(), iv_bytes.data(), key_bytes.data(), buffer.size()); + + std::array result{}; + for (size_t i = 0; i < 16; ++i) { + result[i] = FF(buffer[i]); + } + return result; + } + + /** + * @brief Build an AES128 constraint with specified values. + * + * @param plaintext_vals 16 field elements for plaintext (can include out-of-range values) + * @param key_vals 16 field elements for key + * @param iv_vals 16 field elements for IV + * @param output_vals 16 field elements for expected output + */ + static std::pair create_constraint(const std::array& plaintext_vals, + const std::array& key_vals, + const std::array& iv_vals, + const std::array& output_vals) + { + Builder builder; + + auto add_witness = [&builder](FF value) -> uint32_t { return builder.add_variable(value); }; + + // Add plaintext witnesses + std::vector> input_witnesses; + for (const auto& val : plaintext_vals) { + input_witnesses.push_back(WitnessOrConstant::from_index(add_witness(val))); + } + + // Add key witnesses + std::array, 16> key_witnesses{}; + for (size_t i = 0; i < 16; ++i) { + key_witnesses[i] = WitnessOrConstant::from_index(add_witness(key_vals[i])); + } + + // Add IV witnesses + std::array, 16> iv_witnesses{}; + for (size_t i = 0; i < 16; ++i) { + iv_witnesses[i] = WitnessOrConstant::from_index(add_witness(iv_vals[i])); + } + + // Add output witnesses + std::vector output_indices; + for (const auto& val : output_vals) { + output_indices.push_back(add_witness(val)); + } + + AES128Constraint constraint{ + .inputs = std::move(input_witnesses), + .iv = iv_witnesses, + .key = key_witnesses, + .outputs = std::move(output_indices), + }; + + return { std::move(builder), std::move(constraint) }; + } + + /** + * @brief Helper to test that out-of-range bytes are rejected BY THE CIRCUIT, not by native checks. + * + * This helper does NOT catch exceptions - if a native check throws, the test will fail. + * This ensures we're testing circuit soundness, not native validation. + */ + static bool circuit_rejects_bad_input(Builder& builder, AES128Constraint& constraint) + { + // Don't catch exceptions - we want to verify CIRCUIT constraints catch the issue, + // not native checks that could be bypassed by a malicious prover + create_aes128_constraints(builder, constraint); + return !CircuitChecker::check(builder); + } +}; + +/** + * @brief Test that plaintext byte values > 255 cause circuit failure at the RANGE CONSTRAINT, + * not at the lookup tables. + * + * This tests the "overflow attack" scenario with correct byte ordering: + * - Packing is big-endian: byte[0] is MSB (×256^15), byte[15] is LSB (×256^0) + * - Attacker provides plaintext [..., 0, 256] (256 in LSB position 15) + * - packed = 256 * 256^0 = 256 + * - When sliced: 256 % 256 = 0, 256 / 256 = 1 → slices = [0, 1, 0, ...] + * - This corresponds to valid plaintext [..., 1, 0] (1 in position 14) + * + * The range constraint should catch this attack. + */ +TEST_F(AES128RangeConstraintTest, PlaintextOutOfRangeFails) +{ + // The "overflowed" plaintext that AES would actually see after slicing + // attacker [..., 0, 256] becomes [..., 1, 0] when packed (256) and sliced + std::array overflowed_plaintext = {}; + overflowed_plaintext[14] = FF(1); // Carry from position 15 + overflowed_plaintext[15] = FF(0); // 256 % 256 = 0 + // rest are 0 + + // Compute the ciphertext for the OVERFLOWED plaintext + std::vector buffer(16, 0); + buffer[14] = 1; + buffer[15] = 0; + std::array key_bytes{}; + std::array iv_bytes{}; + for (size_t i = 0; i < 16; ++i) { + key_bytes[i] = static_cast(uint256_t(valid_key[i])); + iv_bytes[i] = static_cast(uint256_t(valid_iv[i])); + } + crypto::aes128_encrypt_buffer_cbc(buffer.data(), iv_bytes.data(), key_bytes.data(), buffer.size()); + + std::array overflowed_ciphertext{}; + for (size_t i = 0; i < 16; ++i) { + overflowed_ciphertext[i] = FF(buffer[i]); + } + + // PART 1: Verify that [..., 1, 0] with matching ciphertext PASSES + // This proves the lookups work fine - there's no issue with the data itself + { + auto [builder, constraint] = + create_constraint(overflowed_plaintext, valid_key, valid_iv, overflowed_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(CircuitChecker::check(builder)) + << "Sanity check: [..., 1, 0] with correct ciphertext should pass (lookups work)"; + } + + // PART 2: Verify that [..., 0, 256] FAILS due to range constraint + // The attacker's plaintext has 256 in LSB position, overflows to [..., 1, 0] + std::array attacker_plaintext = {}; + attacker_plaintext[15] = FF(256); // Out of range in LSB position! + // rest are 0 + + // Attacker provides the ciphertext that matches the overflowed interpretation + auto [builder, constraint] = create_constraint(attacker_plaintext, valid_key, valid_iv, overflowed_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(builder.failed()) << "Circuit should fail when plaintext has byte > 255"; + EXPECT_FALSE(CircuitChecker::check(builder)); +} + +/** + * @brief Test that key byte values > 255 cause circuit failure at the RANGE CONSTRAINT. + * + * Same logic as PlaintextOutOfRangeFails with correct byte ordering: + * - 256 in LSB position (index 15) overflows to 1 in position 14 + */ +TEST_F(AES128RangeConstraintTest, KeyOutOfRangeFails) +{ + // The "overflowed" key that AES would see: [..., 1, 0] + std::array overflowed_key = {}; + overflowed_key[14] = FF(1); // Carry from position 15 + overflowed_key[15] = FF(0); // 256 % 256 = 0 + + // Compute ciphertext with the overflowed key + std::vector buffer(16); + std::array key_bytes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 }; + std::array iv_bytes{}; + for (size_t i = 0; i < 16; ++i) { + buffer[i] = static_cast(uint256_t(valid_plaintext[i])); + iv_bytes[i] = static_cast(uint256_t(valid_iv[i])); + } + crypto::aes128_encrypt_buffer_cbc(buffer.data(), iv_bytes.data(), key_bytes.data(), buffer.size()); + + std::array overflowed_ciphertext{}; + for (size_t i = 0; i < 16; ++i) { + overflowed_ciphertext[i] = FF(buffer[i]); + } + + // PART 1: Verify lookups work with valid key [..., 1, 0] + { + auto [builder, constraint] = + create_constraint(valid_plaintext, overflowed_key, valid_iv, overflowed_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(CircuitChecker::check(builder)) << "Sanity check: key [..., 1, 0] should pass (lookups work)"; + } + + // PART 2: Verify [..., 0, 256] key FAILS due to range constraint + std::array attacker_key = {}; + attacker_key[15] = FF(256); // Out of range in LSB position! + + auto [builder, constraint] = create_constraint(valid_plaintext, attacker_key, valid_iv, overflowed_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(builder.failed()) << "Circuit should fail when key has byte > 255"; + EXPECT_FALSE(CircuitChecker::check(builder)); +} + +/** + * @brief Test that IV byte values > 255 cause circuit failure at the RANGE CONSTRAINT. + * + * Same logic with correct byte ordering: 256 in LSB position overflows to adjacent byte. + */ +TEST_F(AES128RangeConstraintTest, IVOutOfRangeFails) +{ + // The "overflowed" IV that AES would see: [..., 1, 0] + std::array overflowed_iv = {}; + overflowed_iv[14] = FF(1); // Carry from position 15 + overflowed_iv[15] = FF(0); // 256 % 256 = 0 + + // Compute ciphertext with the overflowed IV + std::vector buffer(16); + std::array key_bytes{}; + std::array iv_bytes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 }; + for (size_t i = 0; i < 16; ++i) { + buffer[i] = static_cast(uint256_t(valid_plaintext[i])); + key_bytes[i] = static_cast(uint256_t(valid_key[i])); + } + crypto::aes128_encrypt_buffer_cbc(buffer.data(), iv_bytes.data(), key_bytes.data(), buffer.size()); + + std::array overflowed_ciphertext{}; + for (size_t i = 0; i < 16; ++i) { + overflowed_ciphertext[i] = FF(buffer[i]); + } + + // PART 1: Verify lookups work with valid IV [..., 1, 0] + { + auto [builder, constraint] = + create_constraint(valid_plaintext, valid_key, overflowed_iv, overflowed_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(CircuitChecker::check(builder)) << "Sanity check: IV [..., 1, 0] should pass (lookups work)"; + } + + // PART 2: Verify [..., 0, 256] IV FAILS due to range constraint + std::array attacker_iv = {}; + attacker_iv[15] = FF(256); // Out of range in LSB position! + + auto [builder, constraint] = create_constraint(valid_plaintext, valid_key, attacker_iv, overflowed_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(builder.failed()) << "Circuit should fail when IV has byte > 255"; + EXPECT_FALSE(CircuitChecker::check(builder)); +} + +/** + * @brief Test that output byte values > 255 cause circuit failure at the RANGE CONSTRAINT. + * + * For outputs, we provide witnesses that pack to the same value using LSB overflow: + * If valid output is [..., X, Y], then [..., X-1, Y+256] packs to the same value: + * (X-1)*256^1 + (Y+256)*256^0 = X*256 - 256 + Y + 256 = X*256 + Y + */ +TEST_F(AES128RangeConstraintTest, OutputOutOfRangeFails) +{ + // Compute the valid ciphertext + auto valid_ciphertext = compute_ciphertext(); + + // PART 1: Verify circuit passes with valid output + { + auto [builder, constraint] = create_constraint(valid_plaintext, valid_key, valid_iv, valid_ciphertext); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(CircuitChecker::check(builder)) << "Sanity check: valid ciphertext should pass"; + } + + // PART 2: Create attacker output that packs to the same value using LSB positions + // [..., X-1, Y+256] packs same as [..., X, Y] due to overflow + std::array attacker_output = valid_ciphertext; + uint64_t second_last_byte = static_cast(uint256_t(valid_ciphertext[14])); // X + uint64_t last_byte = static_cast(uint256_t(valid_ciphertext[15])); // Y + + // Need second_last_byte >= 1 to subtract 1 from it + ASSERT_GE(second_last_byte, 1u) << "Test requires ciphertext[14] >= 1"; + + attacker_output[14] = FF(second_last_byte - 1); // X - 1 + attacker_output[15] = FF(last_byte + 256); // Y + 256 (out of range!) + + auto [builder, constraint] = create_constraint(valid_plaintext, valid_key, valid_iv, attacker_output); + create_aes128_constraints(builder, constraint); + EXPECT_TRUE(builder.failed()) << "Circuit should fail when output has byte > 255"; + EXPECT_FALSE(CircuitChecker::check(builder)); +} diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp index 3258b0a41b66..9f2b0acfb6dd 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp @@ -28,7 +28,7 @@ template inline constexpr size_t LOGIC_XOR_32 = 6 + ZERO_GATE template inline constexpr size_t LOGIC_AND_32 = 6 + ZERO_GATE + MEGA_OFFSET; template inline constexpr size_t RANGE_32 = 2744 + ZERO_GATE + MEGA_OFFSET; template inline constexpr size_t SHA256_COMPRESSION = 6702 + ZERO_GATE + MEGA_OFFSET; -template inline constexpr size_t AES128_ENCRYPTION = 1432 + ZERO_GATE + MEGA_OFFSET; +template inline constexpr size_t AES128_ENCRYPTION = 1559 + ZERO_GATE + MEGA_OFFSET; // The mega offset works differently for ECDSA opcodes because of the use of ROM tables, which use indices that // overlap with the values added for ECCVM. secp256k1 uses table of size 16 whose indices contain all the 4 values diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/bn254_crs_data.hpp b/barretenberg/cpp/src/barretenberg/srs/factories/bn254_crs_data.hpp index 135c6c90dc91..ee25dff2517d 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/bn254_crs_data.hpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/bn254_crs_data.hpp @@ -15,7 +15,7 @@ inline constexpr g1::affine_element BN254_G1_FIRST_ELEMENT = g1::affine_one; /** * @brief Expected second G1 element from BN254 CRS * @details This is the second point in the BN254 CRS, corresponding to tau * G where tau is the secret from the - * trusted setup. Reference: http://crs.aztec.network/g1.dat (bytes 64-127) + * trusted setup. Reference: https://crs.aztec-cdn.foundation/g1.dat (bytes 64-127) */ inline g1::affine_element get_bn254_g1_second_element() { @@ -32,7 +32,7 @@ inline g1::affine_element get_bn254_g1_second_element() /** * @brief Reference BN254 G2 element from the trusted setup CRS * @details This is the single G2 point used in the BN254 CRS for verification. - * Reference: http://crs.aztec.network/g2.dat + * Reference: https://crs.aztec-cdn.foundation/g2.dat */ inline g2::affine_element get_bn254_g2_crs_element() { diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp b/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp index a3a1f63be00f..b79ead7c4c13 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp @@ -2,6 +2,8 @@ #include "barretenberg/common/serialize.hpp" #include "barretenberg/ecc/curves/bn254/bn254.hpp" #include "barretenberg/ecc/curves/bn254/pairing.hpp" +#include "barretenberg/srs/factories/bn254_crs_data.hpp" +#include "barretenberg/srs/factories/get_bn254_crs.hpp" #include "barretenberg/srs/factories/mem_bn254_crs_factory.hpp" #include "barretenberg/srs/factories/mem_grumpkin_crs_factory.hpp" #include "barretenberg/srs/factories/native_crs_factory.hpp" @@ -100,3 +102,23 @@ TEST(CrsFactory, grumpkin) ASSERT_ANY_THROW(check_grumpkin_consistency(temp_crs_path, 1, /*allow_download=*/false)); check_grumpkin_consistency(temp_crs_path, 1, /*allow_download=*/true); } + +TEST(CrsFactory, Bn254Fallback) +{ + // Test that fallback works when primary URL fails + const std::filesystem::path& temp_crs_path = "barretenberg_srs_test_crs_bn254_fallback"; + fs::remove_all(temp_crs_path); + fs::create_directories(temp_crs_path); + + // Use a bad primary URL that will fail, forcing fallback to the real S3 URL + std::string bad_primary = "http://nonexistent.invalid/g1.dat"; + std::string good_fallback = "http://crs.aztec-labs.com/g1.dat"; + + // This should succeed by falling back to the working URL + auto points = bb::get_bn254_g1_data(temp_crs_path, 1, /*allow_download=*/true, bad_primary, good_fallback); + EXPECT_EQ(points.size(), 1); + // Verify the downloaded point matches the expected first element + EXPECT_EQ(points[0], bb::srs::BN254_G1_FIRST_ELEMENT); + + fs::remove_all(temp_crs_path); +} diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp b/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp index 396405a87059..8e08f48deace 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.cpp @@ -8,12 +8,33 @@ #include "http_download.hpp" namespace { -std::vector download_bn254_g1_data(size_t num_points) +// Primary CRS URL (Cloudflare R2) +constexpr const char* CRS_PRIMARY_URL = "http://crs.aztec-cdn.foundation/g1.dat"; +// Fallback CRS URL (AWS S3) +constexpr const char* CRS_FALLBACK_URL = "http://crs.aztec-labs.com/g1.dat"; + +std::vector download_bn254_g1_data(size_t num_points, + const std::string& primary_url, + const std::string& fallback_url) { size_t g1_end = (num_points * sizeof(bb::g1::affine_element)) - 1; - // Download via HTTP with Range header - auto data = bb::srs::http_download("http://crs.aztec.network/g1.dat", 0, g1_end); + // Try primary URL first, with fallback on failure. + // Note: WASM is compiled with -fno-exceptions, so try/catch is not available. + // In practice, WASM never calls this function - it initializes CRS via srs_init_srs from JavaScript. + std::vector data; +#ifndef __wasm__ + try { + data = bb::srs::http_download(primary_url, 0, g1_end); + } catch (const std::exception& e) { + vinfo("Primary CRS download failed: ", e.what(), ". Trying fallback..."); + data = bb::srs::http_download(fallback_url, 0, g1_end); + } +#else + // WASM fallback: just try primary (will abort on failure) + data = bb::srs::http_download(primary_url, 0, g1_end); + static_cast(fallback_url); +#endif if (data.size() < sizeof(bb::g1::affine_element)) { throw_or_abort("Downloaded g1 data is too small"); @@ -38,9 +59,13 @@ std::vector download_bn254_g1_data(size_t num_points) } // namespace namespace bb { + +// Main implementation with configurable URLs std::vector get_bn254_g1_data(const std::filesystem::path& path, size_t num_points, - bool allow_download) + bool allow_download, + const std::string& primary_url, + const std::string& fallback_url) { std::filesystem::create_directories(path); @@ -84,7 +109,7 @@ std::vector get_bn254_g1_data(const std::filesystem::path& p } vinfo("downloading bn254 crs..."); - auto data = download_bn254_g1_data(num_points); + auto data = download_bn254_g1_data(num_points, primary_url, fallback_url); write_file(g1_path, data); auto points = std::vector(num_points); @@ -94,4 +119,12 @@ std::vector get_bn254_g1_data(const std::filesystem::path& p return points; } +// Default overload using production URLs +std::vector get_bn254_g1_data(const std::filesystem::path& path, + size_t num_points, + bool allow_download) +{ + return get_bn254_g1_data(path, num_points, allow_download, CRS_PRIMARY_URL, CRS_FALLBACK_URL); +} + } // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.hpp b/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.hpp index f35ebeecd0d9..807c3ef4d60d 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.hpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/get_bn254_crs.hpp @@ -9,5 +9,13 @@ namespace bb { std::vector get_bn254_g1_data(const std::filesystem::path& path, size_t num_points, bool allow_download = true); + +// Overload with custom URLs for testing fallback behavior +std::vector get_bn254_g1_data(const std::filesystem::path& path, + size_t num_points, + bool allow_download, + const std::string& primary_url, + const std::string& fallback_url); + g2::affine_element get_bn254_g2_data(const std::filesystem::path& path, bool allow_download = true); } // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/srs/factories/http_download.hpp b/barretenberg/cpp/src/barretenberg/srs/factories/http_download.hpp index 91c3d117b927..83269ff871d5 100644 --- a/barretenberg/cpp/src/barretenberg/srs/factories/http_download.hpp +++ b/barretenberg/cpp/src/barretenberg/srs/factories/http_download.hpp @@ -28,7 +28,7 @@ namespace bb::srs { /** * @brief Download data from a URL with optional Range header support - * @param url Full URL (e.g., "http://crs.aztec.network/g1.dat") + * @param url Full URL (e.g., "http://crs.aztec-cdn.foundation/g1.dat") * @param start_byte Starting byte for range request (0 for no range) * @param end_byte Ending byte for range request (0 for no range) * @return Downloaded data as bytes @@ -56,7 +56,7 @@ inline std::vector http_download([[maybe_unused]] const std::string& ur std::string path = url.substr(path_start); // Create HTTP client (non-SSL) - httplib::Client cli(("http://" + host).c_str()); + httplib::Client cli("http://" + host); cli.set_follow_location(true); cli.set_connection_timeout(30); cli.set_read_timeout(60); diff --git a/barretenberg/crs/bootstrap.sh b/barretenberg/crs/bootstrap.sh index 5ed9d78045a1..d5b6f45c928b 100755 --- a/barretenberg/crs/bootstrap.sh +++ b/barretenberg/crs/bootstrap.sh @@ -9,6 +9,29 @@ shift || true # 2^25 points + 1 because the first is the generator, *64 bytes per point, -1 because Range is inclusive. # We make the file read only to ensure no test can attempt to grow it any larger. 2^25 is already huge... # TODO: Make bb just download and append/overwrite required range, then it becomes idempotent. + +# Primary CRS host (Cloudflare R2) +CRS_PRIMARY_HOST="https://crs.aztec-cdn.foundation" +# Fallback CRS host (AWS S3) +CRS_FALLBACK_HOST="https://crs.aztec-labs.com" + +# Download with fallback: try primary first, then fallback on failure +download_with_fallback() { + local output="$1" + local file="$2" + local range_header="${3:-}" + + local curl_args=(-s -f -o "$output") + if [ -n "$range_header" ]; then + curl_args+=(-H "Range: $range_header") + fi + + if ! curl "${curl_args[@]}" "${CRS_PRIMARY_HOST}/${file}" 2>/dev/null; then + echo "Primary CRS host failed, trying fallback..." + curl "${curl_args[@]}" "${CRS_FALLBACK_HOST}/${file}" + fi +} + function build { crs_path=$HOME/.bb-crs crs_size=$((2**25+1)) @@ -18,12 +41,11 @@ function build { if [ ! -f "$g1" ] || [ $(stat -c%s "$g1") -lt $crs_size_bytes ]; then echo "Downloading crs of size: ${crs_size} ($((crs_size_bytes/(1024*1024)))MB)" mkdir -p $crs_path - curl -s -H "Range: bytes=0-$((crs_size_bytes-1))" -o $g1 \ - https://crs.aztec.network/g1.dat + download_with_fallback "$g1" "g1.dat" "bytes=0-$((crs_size_bytes-1))" chmod a-w $crs_path/bn254_g1.dat fi if [ ! -f "$g2" ]; then - curl -s https://crs.aztec.network/g2.dat -o $g2 + download_with_fallback "$g2" "g2.dat" fi # TODO: This grumpkin CRS in S3 still has the 28 byte header on it. Remove. @@ -33,8 +55,7 @@ function build { gg1=$crs_path/grumpkin_g1.flat.dat if [ ! -f "$gg1" ] || [ $(stat -c%s "$gg1") -lt $crs_size_bytes ]; then echo "Downloading grumpkin crs of size: ${crs_size} ($((crs_size_bytes/(1024*1024)))MB)" - curl -s -H "Range: bytes=0-$((crs_size_bytes-1))" -o $gg1 \ - https://crs.aztec.network/grumpkin_g1.dat + download_with_fallback "$gg1" "grumpkin_g1.dat" "bytes=0-$((crs_size_bytes-1))" fi } diff --git a/barretenberg/scripts/download_bb_crs.sh b/barretenberg/scripts/download_bb_crs.sh index 676912fee67c..b40babbf2de9 100755 --- a/barretenberg/scripts/download_bb_crs.sh +++ b/barretenberg/scripts/download_bb_crs.sh @@ -6,6 +6,29 @@ set -eu # 2^25 points + 1 because the first is the generator, *64 bytes per point, -1 because Range is inclusive. # We make the file read only to ensure no test can attempt to grow it any larger. 2^25 is already huge... # TODO: Make bb just download and append/overwrite required range, then it becomes idempotent. + +# Primary CRS host (Cloudflare R2) +CRS_PRIMARY_HOST="https://crs.aztec-cdn.foundation" +# Fallback CRS host (AWS S3) +CRS_FALLBACK_HOST="https://crs.aztec-labs.com" + +# Download with fallback: try primary first, then fallback on failure +download_with_fallback() { + local output="$1" + local file="$2" + local range_header="${3:-}" + + local curl_args=(-s -f -o "$output") + if [ -n "$range_header" ]; then + curl_args+=(-H "Range: $range_header") + fi + + if ! curl "${curl_args[@]}" "${CRS_PRIMARY_HOST}/${file}" 2>/dev/null; then + echo "Primary CRS host failed, trying fallback..." + curl "${curl_args[@]}" "${CRS_FALLBACK_HOST}/${file}" + fi +} + crs_path=$HOME/.bb-crs crs_size=$((2**25+1)) crs_size_bytes=$((crs_size*64)) @@ -14,12 +37,11 @@ g2=$crs_path/bn254_g2.dat if [ ! -f "$g1" ] || [ $(stat -c%s "$g1") -lt $crs_size_bytes ]; then echo "Downloading crs of size: ${crs_size} ($((crs_size_bytes/(1024*1024)))MB)" mkdir -p $crs_path - curl -s -H "Range: bytes=0-$((crs_size_bytes-1))" -o $g1 \ - https://crs.aztec.network/g1.dat + download_with_fallback "$g1" "g1.dat" "bytes=0-$((crs_size_bytes-1))" chmod a-w $crs_path/bn254_g1.dat fi if [ ! -f "$g2" ]; then - curl -s https://crs.aztec.network/g2.dat -o $g2 + download_with_fallback "$g2" "g2.dat" fi # TODO: This grumpkin CRS in S3 still has the 28 byte header on it. Remove. @@ -29,6 +51,5 @@ crs_size_bytes=$((crs_size*64)) gg1=$crs_path/grumpkin_g1.flat.dat if [ ! -f "$gg1" ] || [ $(stat -c%s "$gg1") -lt $crs_size_bytes ]; then echo "Downloading grumpkin crs of size: ${crs_size} ($((crs_size_bytes/(1024*1024)))MB)" - curl -s -H "Range: bytes=0-$((crs_size_bytes-1))" -o $gg1 \ - https://crs.aztec.network/grumpkin_g1.dat + download_with_fallback "$gg1" "grumpkin_g1.dat" "bytes=0-$((crs_size_bytes-1))" fi diff --git a/barretenberg/ts/src/crs/net_crs.test.ts b/barretenberg/ts/src/crs/net_crs.test.ts new file mode 100644 index 000000000000..4fa81c6b3dc3 --- /dev/null +++ b/barretenberg/ts/src/crs/net_crs.test.ts @@ -0,0 +1,47 @@ +import { NetCrs, fetchWithFallback } from './net_crs.js'; + +// Expected first G1 point from BN254 CRS (generator point with x=1, y=2 in big-endian) +const BN254_G1_FIRST_ELEMENT = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, +]); + +describe('NetCrs', () => { + it('should download CRS data from primary host', async () => { + const crs = new NetCrs(1); + await crs.init(); + + const g1Data = crs.getG1Data(); + expect(g1Data.length).toBe(64); // 1 point * 64 bytes + + // Verify first point matches expected generator + expect(g1Data).toEqual(BN254_G1_FIRST_ELEMENT); + }, 30000); + + it('should download G2 data', async () => { + const crs = new NetCrs(1); + await crs.init(); + + const g2Data = crs.getG2Data(); + expect(g2Data.length).toBe(128); // G2 point is 128 bytes + }, 30000); +}); + +describe('fetchWithFallback', () => { + it('should fallback to secondary URL when primary fails', async () => { + const badPrimaryUrl = 'https://nonexistent.invalid/g1.dat'; + const goodFallbackUrl = 'https://crs.aztec-labs.com/g1.dat'; + const options: RequestInit = { + headers: { + Range: 'bytes=0-63', + }, + }; + + const response = await fetchWithFallback(badPrimaryUrl, goodFallbackUrl, options); + expect(response.ok || response.status === 206).toBe(true); + + const data = new Uint8Array(await response.arrayBuffer()); + expect(data.length).toBe(64); + expect(data).toEqual(BN254_G1_FIRST_ELEMENT); + }, 30000); +}); diff --git a/barretenberg/ts/src/crs/net_crs.ts b/barretenberg/ts/src/crs/net_crs.ts index 51ce21ad57d8..32db6a1b7ae2 100644 --- a/barretenberg/ts/src/crs/net_crs.ts +++ b/barretenberg/ts/src/crs/net_crs.ts @@ -1,4 +1,30 @@ import { retry, makeBackoff } from '../retry/index.js'; + +// Primary CRS host (Cloudflare R2) +const CRS_PRIMARY_HOST = 'https://crs.aztec-cdn.foundation'; +// Fallback CRS host (AWS S3) +const CRS_FALLBACK_HOST = 'https://crs.aztec-labs.com'; + +/** + * Fetches data from primary URL, falling back to secondary on failure + * @internal Exported for testing + */ +export async function fetchWithFallback( + primaryUrl: string, + fallbackUrl: string, + options: RequestInit, +): Promise { + try { + const response = await fetch(primaryUrl, options); + if (response.ok || response.status === 206) { + return response; + } + throw new Error(`HTTP ${response.status}`); + } catch { + return await fetch(fallbackUrl, options); + } +} + /** * Downloader for CRS from the web or local. */ @@ -76,14 +102,14 @@ export class NetCrs { } const g1End = this.numPoints * 64 - 1; + const options: RequestInit = { + headers: { + Range: `bytes=0-${g1End}`, + }, + cache: 'force-cache', + }; return await retry( - () => - fetch('https://crs.aztec.network/g1.dat', { - headers: { - Range: `bytes=0-${g1End}`, - }, - cache: 'force-cache', - }), + () => fetchWithFallback(`${CRS_PRIMARY_HOST}/g1.dat`, `${CRS_FALLBACK_HOST}/g1.dat`, options), makeBackoff([5, 5, 5]), ); } @@ -92,11 +118,11 @@ export class NetCrs { * Fetches the appropriate range of points from a remote source */ private async fetchG2Data(): Promise { + const options: RequestInit = { + cache: 'force-cache', + }; return await retry( - () => - fetch('https://crs.aztec.network/g2.dat', { - cache: 'force-cache', - }), + () => fetchWithFallback(`${CRS_PRIMARY_HOST}/g2.dat`, `${CRS_FALLBACK_HOST}/g2.dat`, options), makeBackoff([5, 5, 5]), ); } @@ -153,12 +179,17 @@ export class NetGrumpkinCrs { } const g1End = this.numPoints * 64 - 1; - - return await fetch('https://crs.aztec.network/grumpkin_g1.dat', { + const options: RequestInit = { headers: { Range: `bytes=0-${g1End}`, }, cache: 'force-cache', - }); + }; + + return await fetchWithFallback( + `${CRS_PRIMARY_HOST}/grumpkin_g1.dat`, + `${CRS_FALLBACK_HOST}/grumpkin_g1.dat`, + options, + ); } }