|
1 | 1 | #include "barretenberg/vm2/constraining/verifier.hpp" |
2 | 2 | #include "barretenberg/srs/global_crs.hpp" |
| 3 | +#include "barretenberg/vm2/common/constants.hpp" |
| 4 | +#include "barretenberg/vm2/constraining/check_circuit.hpp" |
| 5 | +#include "barretenberg/vm2/constraining/polynomials.hpp" |
3 | 6 | #include "barretenberg/vm2/constraining/prover.hpp" |
| 7 | +#include "barretenberg/vm2/generated/columns.hpp" |
4 | 8 | #include "barretenberg/vm2/proving_helper.hpp" |
5 | 9 | #include "barretenberg/vm2/testing/fixtures.hpp" |
6 | 10 |
|
@@ -72,6 +76,130 @@ TEST_F(AvmVerifierTests, NegativeBadPublicInputs) |
72 | 76 | ASSERT_TRUE(verified) << "native proof verification failed, but should have succeeded"; |
73 | 77 | } |
74 | 78 |
|
| 79 | +// Attacker simulation: commit honestly to keccak_memory_addr, but in sumcheck use a DIFFERENT |
| 80 | +// (independent) polynomial for keccak_memory_addr_shift whose last-row value is non-zero. |
| 81 | +// The honest prover's `key_poly.shifted()` shares memory with the unshifted polynomial and its |
| 82 | +// end_index is (unshifted.end_index - 1) <= N - 1, so the last-row shifted value is always past |
| 83 | +// the shifted polynomial's end and thus virtually zero. |
| 84 | +// A malicious prover can replace this shifted view after AvmProver construction to try and |
| 85 | +// smuggle a non-zero value at the last row. This test verifies that the PCS (Shplemini) catches |
| 86 | +// the mismatch between the malicious shifted evaluation used in sumcheck and the real shift of |
| 87 | +// the commitment, causing the verifier to reject. |
| 88 | +TEST_F(AvmVerifierTests, ProvingSystemSecurityShiftedLastRowMustBeZero) |
| 89 | +{ |
| 90 | + if (testing::skip_slow_tests()) { |
| 91 | + GTEST_SKIP() << "Skipping slow test"; |
| 92 | + } |
| 93 | + |
| 94 | + auto [trace, public_inputs] = testing::get_minimal_trace_with_pi(); |
| 95 | + // Capture the number of witness rows before compute_polynomials consumes the trace. |
| 96 | + const size_t num_witness_rows = trace.get_num_witness_rows() + 1; |
| 97 | + |
| 98 | + auto polynomials = constraining::compute_polynomials(trace); |
| 99 | + auto proving_key = constraining::proving_key_from_polynomials(polynomials); |
| 100 | + auto verification_key = std::make_shared<AvmVerifier::VerificationKey>(); |
| 101 | + |
| 102 | + AvmProver prover(proving_key, verification_key, proving_key->commitment_key); |
| 103 | + |
| 104 | + // Attacker: overwrite the shifted view with an independent polynomial carrying a non-zero |
| 105 | + // value at the last row of the circuit. The shared-memory link to the unshifted polynomial |
| 106 | + // is severed, so the unshifted commitment no longer "agrees" with what sumcheck uses. |
| 107 | + using Polynomial = AvmFlavor::Polynomial; |
| 108 | + auto make_malicious_shift = [] { |
| 109 | + Polynomial p(/*size=*/1, /*virtual_size=*/MAX_AVM_TRACE_SIZE, /*start_index=*/MAX_AVM_TRACE_SIZE - 1); |
| 110 | + p.at(MAX_AVM_TRACE_SIZE - 1) = FF(FF::modulus - 1); |
| 111 | + return p; |
| 112 | + }; |
| 113 | + prover.prover_polynomials.get(ColumnAndShifts::keccak_memory_addr_shift) = make_malicious_shift(); |
| 114 | + |
| 115 | + // Sanity: all relations (main + lookup/permutation) still hold with the attacker's |
| 116 | + // polynomials. This demonstrates that any subsequent verification failure is NOT due to a |
| 117 | + // relation violation but to the proving system's cryptographic shift-consistency check |
| 118 | + // catching the forged shifted value. |
| 119 | + AvmFlavor::ProverPolynomials check_polys(*proving_key); |
| 120 | + check_polys.get(ColumnAndShifts::keccak_memory_addr_shift) = make_malicious_shift(); |
| 121 | + ASSERT_NO_THROW(constraining::run_check_circuit(check_polys, num_witness_rows, /*skippable_enabled=*/true)); |
| 122 | + |
| 123 | + const auto proof = prover.construct_proof(); |
| 124 | + |
| 125 | + Verifier verifier; |
| 126 | + const bool verified = verifier.verify_proof(proof, public_inputs.to_columns()); |
| 127 | + |
| 128 | + ASSERT_FALSE(verified) |
| 129 | + << "verifier accepted a proof where keccak_memory_addr_shift at the last row was forged to be non-zero"; |
| 130 | +} |
| 131 | + |
| 132 | +// Symmetric attacker simulation for the UNSHIFTED polynomial at its first row (index 0). |
| 133 | +// Unlike the shifted-last-row case, this test is DISABLED by default because it cannot run |
| 134 | +// against an unmodified barretenberg tree. The attack path uses a polynomial with |
| 135 | +// start_index = 0, which triggers invariants enforced by the honest prover's polynomial |
| 136 | +// library. To run this test the following safeguards in |
| 137 | +// `barretenberg/cpp/src/barretenberg/polynomials/polynomial.cpp` must be relaxed: |
| 138 | +// |
| 139 | +// 1. `Polynomial::add_scaled` (and `Polynomial::operator+=`): the two asserts |
| 140 | +// `BB_ASSERT_LTE(start_index(), other.start_index)` and |
| 141 | +// `BB_ASSERT_GTE(end_index(), other.end_index())` fire during the PCS batching step in |
| 142 | +// `execute_pcs_rounds` because the accumulator has start_index = 1 while the malicious |
| 143 | +// polynomial has start_index = 0. Replace the asserts with a left/right expansion of |
| 144 | +// self's backing memory (using `_clone(..., left_expansion)` / |
| 145 | +// `_clone(..., right_expansion)`) so that the malicious value at index 0 contributes |
| 146 | +// to the batched polynomial consistently. |
| 147 | +// |
| 148 | +// 2. `Polynomial::shifted`: asserts `start_ >= 1` because the Gemini shift |
| 149 | +// (`A_0 = F + G/X`) is only well-defined when the polynomial has zero constant term. |
| 150 | +// To keep the attacker-prover running past this point, special-case start_ == 0 by |
| 151 | +// cloning the backing memory and dropping the first element. This is the same |
| 152 | +// algebraic step that, on the verifier side, makes the proof unverifiable: the |
| 153 | +// committed polynomial and the PCS-derived shifted opening cannot both be consistent |
| 154 | +// when f[0] != 0. |
| 155 | +// |
| 156 | +// Once those patches are applied, this test passes (the verifier rejects the forged proof), |
| 157 | +// confirming that the proving system structurally enforces `f[0] = 0` for any polynomial |
| 158 | +// that is referenced in shifted form — independent of any PIL relation — thanks to the |
| 159 | +// non-cyclic multilinear shift in the PCS. |
| 160 | +TEST_F(AvmVerifierTests, DISABLED_ProvingSystemSecurityUnshiftedFirstRowMustBeZero) |
| 161 | +{ |
| 162 | + if (testing::skip_slow_tests()) { |
| 163 | + GTEST_SKIP() << "Skipping slow test"; |
| 164 | + } |
| 165 | + |
| 166 | + auto [trace, public_inputs] = testing::get_minimal_trace_with_pi(); |
| 167 | + const size_t num_witness_rows = trace.get_num_witness_rows() + 1; |
| 168 | + |
| 169 | + auto polynomials = constraining::compute_polynomials(trace); |
| 170 | + auto proving_key = constraining::proving_key_from_polynomials(polynomials); |
| 171 | + auto verification_key = std::make_shared<AvmVerifier::VerificationKey>(); |
| 172 | + |
| 173 | + AvmProver prover(proving_key, verification_key, proving_key->commitment_key); |
| 174 | + |
| 175 | + // Attacker: overwrite the unshifted polynomial with an independent polynomial carrying a |
| 176 | + // non-zero value at index 0. The honest shifted view is left untouched in |
| 177 | + // prover_polynomials so we can observe what the verifier does with an inconsistent pair. |
| 178 | + using Polynomial = AvmFlavor::Polynomial; |
| 179 | + auto make_malicious_addr = [] { |
| 180 | + Polynomial p(/*size=*/1024, /*virtual_size=*/MAX_AVM_TRACE_SIZE, /*start_index=*/0); |
| 181 | + p.at(0) = FF(FF::modulus - 1); |
| 182 | + return p; |
| 183 | + }; |
| 184 | + prover.prover_polynomials.get(ColumnAndShifts::keccak_memory_addr) = make_malicious_addr(); |
| 185 | + |
| 186 | + // Sanity: all relations (main + lookup/permutation) still hold with the attacker's |
| 187 | + // polynomials. This demonstrates that any subsequent verification failure is NOT due to a |
| 188 | + // relation violation but to the proving system's cryptographic shift-consistency check |
| 189 | + // catching the forged first-row value. |
| 190 | + AvmFlavor::ProverPolynomials check_polys(*proving_key); |
| 191 | + check_polys.get(ColumnAndShifts::keccak_memory_addr) = make_malicious_addr(); |
| 192 | + ASSERT_NO_THROW(constraining::run_check_circuit(check_polys, num_witness_rows, /*skippable_enabled=*/true)); |
| 193 | + |
| 194 | + const auto proof = prover.construct_proof(); |
| 195 | + |
| 196 | + Verifier verifier; |
| 197 | + const bool verified = verifier.verify_proof(proof, public_inputs.to_columns()); |
| 198 | + |
| 199 | + ASSERT_FALSE(verified) |
| 200 | + << "verifier accepted a proof where keccak_memory_addr at the first row was forged to be non-zero"; |
| 201 | +} |
| 202 | + |
75 | 203 | // Verify that the actual proof size matches COMPUTED_AVM_PROOF_LENGTH_IN_FIELDS |
76 | 204 | TEST_F(AvmVerifierTests, ProofSizeMatchesComputedConstant) |
77 | 205 | { |
|
0 commit comments