From f364d9befcc44e61c772b2db82b59f7cefd5dc1e Mon Sep 17 00:00:00 2001 From: notnotraju Date: Wed, 25 Feb 2026 10:58:13 +0000 Subject: [PATCH 1/3] opcode_is_zero means that the next accumulator is empty. --- .../dsl/acir_format/gate_count_constants.hpp | 4 +-- .../ecc_vm/ecc_transcript_relation_impl.hpp | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) 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 2e9442c984dc..7a95529d1299 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 @@ -113,7 +113,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( // ======================================== // Gate count for Chonk recursive verification (Ultra with RollupIO) -inline constexpr size_t CHONK_RECURSION_GATES = 1684546; +inline constexpr size_t CHONK_RECURSION_GATES = 1684787; // ======================================== // Hypernova Recursion Constants @@ -147,7 +147,7 @@ inline constexpr size_t HIDING_KERNEL_ULTRA_OPS = 124; // ======================================== // Gate count for ECCVM recursive verifier (Ultra-arithmetized) -inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 224139; +inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 224380; // ======================================== // Goblin AVM Recursive Verifier Constants diff --git a/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp b/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp index a29e55ed6b14..04d11a77293f 100644 --- a/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp @@ -479,11 +479,20 @@ void ECCVMTranscriptRelationImpl::accumulate(ContainerOverSubrelations& accu } /** - * @brief Validate `is_accumulator_empty` is updated correctly - * An add operation can produce a point at infinity - * Resetting the accumulator produces a point at infinity - * If we are not adding, performing an msm or resetting the accumulator (or doing a no-op), - * is_accumulator_empty should not update + * @brief Validate `is_accumulator_empty` is updated correctly. + * + * The relation is a sum of four mutually exclusive terms, each constraining `is_accumulator_empty_shift` + * for a specific case: + * (A) accumulator_infinity_preserve: active when propagate_transcript_accumulator != 0 + * (i.e. q_mul without msm_transition, or q_eq without q_reset). Preserves the emptiness flag. + * (B) accumulator_infinity_q_reset: active when q_reset_accumulator = 1. Forces empty. + * (B) accumulator_infinity_from_add: active when any_add_is_active != 0 (q_add or msm_transition). + * Sets emptiness from the add/msm result. + * (C) accumulator_infinity_from_noop: active when opcode_is_zero != 0 (all selectors off). Forces empty. + * + * These are mutually exclusive because opcode_is_zero requires all selectors = 0 (which zeros out A and B), + * while A and B each require at least one selector to be non-zero (which zeros out C). + * Within A and B, exclusivity follows from the opcode exclusion constraint (subrelation 8). */ auto accumulator_infinity_preserve_flag = propagate_transcript_accumulator; // degree 1 auto accumulator_infinity_preserve = accumulator_infinity_preserve_flag * @@ -492,10 +501,19 @@ void ECCVMTranscriptRelationImpl::accumulate(ContainerOverSubrelations& accu auto accumulator_infinity_q_reset = q_reset_accumulator * (-is_accumulator_empty_shift + 1); // degree 2 auto accumulator_infinity_from_add = any_add_is_active * (result_is_infinity - is_accumulator_empty_shift); // degree 3 + // When opcode_is_zero (no-op row), the accumulator output is forced to (0,0) by lines 428-429. + // We must also force is_accumulator_empty_shift = 1 so that the emptiness flag is consistent + // with the (0,0) accumulator coordinates. Without this, a malicious prover could set + // accumulator_not_empty = 1 on the next row while the accumulator is (0,0), creating an + // inconsistency that bypasses on-curve checks (which are only performed on input coordinates). + auto opcode_is_zero = + (is_not_first_row) * (-q_add + 1) * (-q_mul + 1) * (-q_reset_accumulator + 1) * (-q_eq + 1); // degree 5 + auto accumulator_infinity_from_noop = opcode_is_zero * (-is_accumulator_empty_shift + 1); // degree 6 auto accumulator_infinity_relation = accumulator_infinity_preserve + - (accumulator_infinity_q_reset + accumulator_infinity_from_add) * is_not_first_row; // degree 4 - std::get<22>(accumulator) += accumulator_infinity_relation * scaling_factor; // degree 4 + (accumulator_infinity_q_reset + accumulator_infinity_from_add) * is_not_first_row + + accumulator_infinity_from_noop; // degree 6 + std::get<22>(accumulator) += accumulator_infinity_relation * scaling_factor; // degree 6 /** * @brief Validate `transcript_add_x_equal` is well-formed From e523e1e8544365b9ff9690dee324228b178ad575 Mon Sep 17 00:00:00 2001 From: notnotraju Date: Thu, 5 Mar 2026 17:00:07 +0000 Subject: [PATCH 2/3] update gate counts --- .../src/barretenberg/dsl/acir_format/gate_count_constants.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7c0bffa7bf09..d567db14dd45 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 @@ -113,7 +113,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( // ======================================== // Gate count for Chonk recursive verification (Ultra with RollupIO) -inline constexpr size_t CHONK_RECURSION_GATES = 1684865; +inline constexpr size_t CHONK_RECURSION_GATES = 1685107; // ======================================== // Hypernova Recursion Constants @@ -147,7 +147,7 @@ inline constexpr size_t HIDING_KERNEL_ULTRA_OPS = 124; // ======================================== // Gate count for ECCVM recursive verifier (Ultra-arithmetized) -inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 224458; +inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 224702; // ======================================== // Goblin AVM Recursive Verifier Constants From 52e192246355f884d05b8d9fba76b7eef2ed9d6f Mon Sep 17 00:00:00 2001 From: notnotraju Date: Thu, 5 Mar 2026 17:28:22 +0000 Subject: [PATCH 3/3] added precise failure test showing the behavior is as desired --- .../eccvm/eccvm_relation_corruption.test.cpp | 47 +++++++++++++++++++ .../ecc_vm/ecc_transcript_relation_impl.hpp | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_relation_corruption.test.cpp b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_relation_corruption.test.cpp index dfa4673a710f..d7b565313e90 100644 --- a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_relation_corruption.test.cpp +++ b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_relation_corruption.test.cpp @@ -128,6 +128,22 @@ RelationParameters compute_full_relation_params(ProverPolynomials& polynomia return params; } +/** + * @brief Find the first transcript no-op row: all selectors zero, not first/last row. + */ +size_t find_transcript_noop_row(const ProverPolynomials& polynomials) +{ + const size_t num_rows = polynomials.get_polynomial_size(); + for (size_t i = 2; i < num_rows - 1; i++) { + if (polynomials.transcript_add[i] == FF(0) && polynomials.transcript_mul[i] == FF(0) && + polynomials.transcript_eq[i] == FF(0) && polynomials.transcript_reset_accumulator[i] == FF(0) && + polynomials.lagrange_first[i] == FF(0) && polynomials.lagrange_last[i] == FF(0)) { + return i; + } + } + return 0; +} + } // anonymous namespace class ECCVMRelationCorruptionTests : public ::testing::Test { @@ -330,3 +346,34 @@ TEST_F(ECCVMRelationCorruptionTests, MSMRelationFailsOnShiftedMSMTable) polynomials, full_params, "ECCVMLookupRelation"); EXPECT_TRUE(lookup_failures.empty()) << "ECCVMLookupRelation should still pass (inverse computed post-shift)"; } + +/** + * @brief On a transcript no-op row, setting accumulator_not_empty=1 must be caught by subrelation 22. + * + * @details The `accumulator_infinity_from_noop` term in subrelation 22 forces + * is_accumulator_empty_shift = 1 whenever all selectors are zero. This test corrupts + * the shifted value (i.e. accumulator_not_empty at row+1) to 1 and verifies detection. + */ +TEST_F(ECCVMRelationCorruptionTests, TranscriptNoOpRowRejectsAccumulatorNotEmpty) +{ + auto polynomials = build_valid_eccvm_msm_state(); + RelationParameters params{}; + + auto baseline = + RelationChecker::check>(polynomials, params, "ECCVMTranscriptRelation"); + EXPECT_TRUE(baseline.empty()) << "Baseline transcript relation should pass"; + + size_t noop_row = find_transcript_noop_row(polynomials); + ASSERT_NE(noop_row, 0) << "Should find a transcript no-op row"; + + // The no-op constraint at row `noop_row` constrains is_accumulator_empty_shift, + // which reads from accumulator_not_empty at row `noop_row + 1`. + polynomials.transcript_accumulator_not_empty.at(noop_row + 1) = FF(1); + polynomials.set_shifted(); + + auto failures = + RelationChecker::check>(polynomials, params, "ECCVMTranscriptRelation"); + EXPECT_FALSE(failures.empty()) << "Transcript relation should fail after corrupting accumulator_not_empty on " + "the row following a no-op"; + EXPECT_TRUE(failures.contains(22)) << "Subrelation 22 (accumulator_infinity) should catch the corruption"; +} diff --git a/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp b/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp index 04d11a77293f..626397c46f96 100644 --- a/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/ecc_vm/ecc_transcript_relation_impl.hpp @@ -501,7 +501,7 @@ void ECCVMTranscriptRelationImpl::accumulate(ContainerOverSubrelations& accu auto accumulator_infinity_q_reset = q_reset_accumulator * (-is_accumulator_empty_shift + 1); // degree 2 auto accumulator_infinity_from_add = any_add_is_active * (result_is_infinity - is_accumulator_empty_shift); // degree 3 - // When opcode_is_zero (no-op row), the accumulator output is forced to (0,0) by lines 428-429. + // When opcode_is_zero (no-op row), the accumulator output is forced to (0,0) by subrelations 15 and 16. // We must also force is_accumulator_empty_shift = 1 so that the emptiness flag is consistent // with the (0,0) accumulator coordinates. Without this, a malicious prover could set // accumulator_not_empty = 1 on the next row while the accumulator is (0,0), creating an