Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
storage_write.test.cpp
Go to the documentation of this file.
1#include <gmock/gmock.h>
2#include <gtest/gtest.h>
3
4#include <cstdint>
5
6#include "barretenberg/aztec/aztec_constants.hpp"
30
31namespace bb::avm2::constraining {
32namespace {
33
34using tracegen::ExecutionTraceBuilder;
35using tracegen::IndexedTreeCheckTraceBuilder;
36using tracegen::PublicDataTreeTraceBuilder;
37using tracegen::TestTraceContainer;
38
40using simulation::EventEmitter;
41using simulation::IndexedTreeCheck;
43using simulation::MockExecutionIdManager;
44using simulation::MockFieldGreaterThan;
45using simulation::MockMerkleCheck;
46using simulation::MockPoseidon2;
47using simulation::PublicDataTreeCheck;
52using simulation::WrittenPublicDataSlotsTreeCheck;
53
54using testing::_;
55using testing::NiceMock;
56
58using C = Column;
59using sstore = bb::avm2::sstore<FF>;
62
63TEST(SStoreConstrainingTest, PositiveTest)
64{
65 TestTraceContainer trace({
66 { { C::execution_sel_execute_sstore, 1 },
67 { C::execution_sel_gas_sstore, 1 },
68 { C::execution_dynamic_da_gas_factor, 1 },
69 { C::execution_register_0_, /*value=*/27 },
70 { C::execution_register_1_, /*slot=*/42 },
71 { C::execution_prev_written_public_data_slots_tree_size, 5 },
72 { C::execution_max_data_writes_reached, 0 },
73 { C::execution_remaining_data_writes_inv,
74 FF(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE - 5).invert() },
75 { C::execution_sel_write_public_data, 1 },
76 { C::execution_subtrace_operation_id, AVM_EXEC_OP_ID_SSTORE } },
77 });
78 check_relation<sstore>(trace);
79}
80
81TEST(SStoreConstrainingTest, NegativeDynamicL2GasIsZero)
82{
83 TestTraceContainer trace({ {
84 { C::execution_sel_execute_sstore, 1 },
85 { C::execution_dynamic_l2_gas_factor, 1 },
86 } });
87 EXPECT_THROW_WITH_MESSAGE(check_relation<execution>(trace, execution::SR_DYN_L2_GAS_IS_ZERO),
89}
90
91TEST(SStoreConstrainingTest, MaxDataWritesReached)
92{
93 TestTraceContainer trace({
94 {
95 { C::execution_sel_execute_sstore, 1 },
96 { C::execution_prev_written_public_data_slots_tree_size,
97 MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE },
98 { C::execution_remaining_data_writes_inv, 0 },
99 { C::execution_max_data_writes_reached, 1 },
100 },
101 });
102 check_relation<sstore>(trace, sstore::SR_SSTORE_MAX_DATA_WRITES_REACHED);
103
104 trace.set(C::execution_max_data_writes_reached, 0, 0);
105
108}
109
110TEST(SStoreConstrainingTest, OpcodeError)
111{
112 TestTraceContainer trace({
113 {
114 { C::execution_sel_execute_sstore, 1 },
115 { C::execution_dynamic_da_gas_factor, 1 },
116 { C::execution_max_data_writes_reached, 1 },
117 { C::execution_sel_opcode_error, 1 },
118 },
119 {
120 { C::execution_sel_execute_sstore, 1 },
121 { C::execution_dynamic_da_gas_factor, 0 },
122 { C::execution_max_data_writes_reached, 0 },
123 { C::execution_is_static, 1 },
124 { C::execution_sel_opcode_error, 1 },
125 },
126 {
127 { C::execution_sel_execute_sstore, 1 },
128 { C::execution_dynamic_da_gas_factor, 0 },
129 { C::execution_max_data_writes_reached, 1 },
130 { C::execution_sel_opcode_error, 0 },
131 },
132 });
133 check_relation<sstore>(trace, sstore::SR_OPCODE_ERROR_IF_OVERFLOW_OR_STATIC);
134
135 trace.set(C::execution_dynamic_da_gas_factor, 0, 0);
136
139
140 trace.set(C::execution_dynamic_da_gas_factor, 0, 1);
141
142 trace.set(C::execution_is_static, 1, 0);
143
146}
147
148TEST(SStoreConstrainingTest, TreeStateNotChangedOnError)
149{
150 TestTraceContainer trace({ {
151 { C::execution_sel_execute_sstore, 1 },
152 { C::execution_prev_public_data_tree_root, 27 },
153 { C::execution_prev_public_data_tree_size, 5 },
154 { C::execution_prev_written_public_data_slots_tree_root, 28 },
155 { C::execution_prev_written_public_data_slots_tree_size, 6 },
156 { C::execution_public_data_tree_root, 27 },
157 { C::execution_public_data_tree_size, 5 },
158 { C::execution_written_public_data_slots_tree_root, 28 },
159 { C::execution_written_public_data_slots_tree_size, 6 },
160 { C::execution_sel_opcode_error, 1 },
161 } });
162
163 check_relation<sstore>(trace,
168
169 // Negative test: written slots tree root must be the same
170 trace.set(C::execution_written_public_data_slots_tree_root, 0, 29);
173
174 // Negative test: written slots tree size must be the same
175 trace.set(C::execution_written_public_data_slots_tree_size, 0, 7);
178
179 // Negative test: public data tree root must be the same
180 trace.set(C::execution_public_data_tree_root, 0, 29);
183
184 // Negative test: public data tree size must be the same
185 trace.set(C::execution_public_data_tree_size, 0, 7);
188}
189
190// Test that ghost rows (sel_execute_sstore=0) cannot set sel_write_public_data=1
191// This verifies the fix: sel_write_public_data * (1 - sel_execute_sstore) = 0
192TEST(SStoreConstrainingTest, NegativeGhostRowStorageWrite_RelationsOnly)
193{
194 // Try to create a ghost row (sel_execute_sstore=0) with sel_write_public_data=1
195 TestTraceContainer trace({
196 {
197 { C::execution_sel_execute_sstore, 0 }, // Ghost row: sstore not executing
198 { C::execution_sel_write_public_data, 1 }, // Try to fire storage write anyway
199 { C::execution_register_0_, /*value=*/999 }, // Arbitrary value
200 { C::execution_register_1_, /*slot=*/666 }, // Arbitrary slot
201 { C::execution_contract_address, 0xDEADBEEF }, // Arbitrary address
202 { C::execution_sel_opcode_error, 0 },
203 },
204 });
205
206 // The fix: sel_write_public_data = sel_execute_sstore * (1 - sel_opcode_error)
207 // When sel_execute_sstore=0 and sel_write_public_data=1: 1 * (1-0) = 1 != 0 -> FAILS
208 EXPECT_THROW_WITH_MESSAGE(check_relation<sstore>(trace),
210}
211
212TEST(SStoreConstrainingTest, Interactions)
213{
214 NiceMock<MockPoseidon2> poseidon2;
215 NiceMock<MockFieldGreaterThan> field_gt;
216 NiceMock<MockMerkleCheck> merkle_check;
217 NiceMock<MockExecutionIdManager> execution_id_manager;
218
219 EventEmitter<IndexedTreeCheckEvent> indexed_tree_check_emitter;
220 IndexedTreeCheck indexed_tree_check(
221 poseidon2, merkle_check, field_gt, DOM_SEP__WRITTEN_SLOTS_MERKLE, indexed_tree_check_emitter);
222
223 WrittenPublicDataSlotsTreeCheck written_public_data_slots_tree_check(indexed_tree_check,
225
226 EventEmitter<PublicDataTreeCheckEvent> public_data_tree_check_event_emitter;
227 PublicDataTreeCheck public_data_tree_check(
228 poseidon2, merkle_check, field_gt, execution_id_manager, public_data_tree_check_event_emitter);
229
230 FF slot = 42;
231 AztecAddress contract_address = 1;
232 FF leaf_slot = unconstrained_compute_leaf_slot(contract_address, slot);
233 FF value = 27;
234
236 uint64_t low_leaf_index = 30;
237 std::vector<FF> low_leaf_sibling_path = { 1, 2, 3, 4, 5 };
238
239 AppendOnlyTreeSnapshot public_data_tree_before = AppendOnlyTreeSnapshot{
240 .root = 42,
241 .next_available_leaf_index = 128,
242 };
243 AppendOnlyTreeSnapshot written_slots_tree_before = written_public_data_slots_tree_check.get_snapshot();
244
245 EXPECT_CALL(poseidon2, hash(_)).WillRepeatedly([](const std::vector<FF>& inputs) {
247 });
248 EXPECT_CALL(field_gt, ff_gt(_, _)).WillRepeatedly([](const FF& a, const FF& b) {
249 return static_cast<uint256_t>(a) > static_cast<uint256_t>(b);
250 });
251
252 EXPECT_CALL(merkle_check, write)
253 .WillRepeatedly([]([[maybe_unused]] uint64_t domain_separator,
254 [[maybe_unused]] FF current_leaf,
255 FF new_leaf,
256 uint64_t leaf_index,
257 std::span<const FF> sibling_path,
258 [[maybe_unused]] FF prev_root) {
259 return unconstrained_root_from_path(DOM_SEP__WRITTEN_SLOTS_MERKLE, new_leaf, leaf_index, sibling_path);
260 });
261
262 written_public_data_slots_tree_check.contains(contract_address, slot);
263
264 auto public_data_tree_after = public_data_tree_check.write(slot,
265 contract_address,
266 value,
267 low_leaf,
268 low_leaf_index,
269 low_leaf_sibling_path,
270 public_data_tree_before,
271 {},
272 false);
273 written_public_data_slots_tree_check.insert(contract_address, slot);
274 auto written_slots_tree_after = written_public_data_slots_tree_check.get_snapshot();
275
276 TestTraceContainer trace({
277 {
278 { C::execution_sel_execute_sstore, 1 },
279 { C::execution_contract_address, contract_address },
280 { C::execution_sel_gas_sstore, 1 },
281 { C::execution_written_slots_tree_height, AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_HEIGHT },
282 { C::execution_written_slots_merkle_separator, DOM_SEP__WRITTEN_SLOTS_MERKLE },
283 { C::execution_written_slots_tree_siloing_separator, DOM_SEP__PUBLIC_LEAF_SLOT },
284 { C::execution_dynamic_da_gas_factor, 1 },
285 { C::execution_register_0_, value },
286 { C::execution_register_1_, slot },
287 { C::execution_max_data_writes_reached, 0 },
288 { C::execution_remaining_data_writes_inv,
289 FF(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE -
290 written_slots_tree_before.next_available_leaf_index)
291 .invert() },
292 { C::execution_subtrace_operation_id, AVM_EXEC_OP_ID_SSTORE },
293 { C::execution_sel_write_public_data, 1 },
294 { C::execution_prev_public_data_tree_root, public_data_tree_before.root },
295 { C::execution_prev_public_data_tree_size, public_data_tree_before.next_available_leaf_index },
296 { C::execution_public_data_tree_root, public_data_tree_after.root },
297 { C::execution_public_data_tree_size, public_data_tree_after.next_available_leaf_index },
298 { C::execution_prev_written_public_data_slots_tree_root, written_slots_tree_before.root },
299 { C::execution_prev_written_public_data_slots_tree_size,
300 written_slots_tree_before.next_available_leaf_index },
301 { C::execution_written_public_data_slots_tree_root, written_slots_tree_after.root },
302 { C::execution_written_public_data_slots_tree_size, written_slots_tree_after.next_available_leaf_index },
303 },
304 });
305
306 PublicDataTreeTraceBuilder public_data_tree_trace_builder;
307 public_data_tree_trace_builder.process(public_data_tree_check_event_emitter.dump_events(), trace);
308
309 IndexedTreeCheckTraceBuilder written_slots_tree_trace_builder;
310 written_slots_tree_trace_builder.process(indexed_tree_check_emitter.dump_events(), trace);
311
312 check_relation<sstore>(trace);
313 check_interaction<ExecutionTraceBuilder,
316 check_multipermutation_interaction<PublicDataTreeTraceBuilder,
319}
320
321// Ghost row injection attack test.
322// Verifies that the fix (sel_write_public_data * (1 - sel_execute_sstore) = 0) prevents
323// a malicious prover from injecting arbitrary storage writes via ghost sstore rows.
324//
325// Attack vector (now blocked):
326// 1. Create ghost sstore row (sel_execute_sstore=0, sel_write_public_data=1)
327// 2. Populate public_data_check trace with legitimate rows via simulation
328// 3. Align clk values so the STORAGE_WRITE permutation matches
329// 4. Without the fix, the permutation would pass and arbitrary writes would be possible
330TEST(SStoreConstrainingTest, NegativeFullAttackWithAllTraces)
331{
332 NiceMock<MockPoseidon2> poseidon2;
333 NiceMock<MockFieldGreaterThan> field_gt;
334 NiceMock<MockMerkleCheck> merkle_check;
335 NiceMock<MockExecutionIdManager> execution_id_manager;
336
337 EventEmitter<IndexedTreeCheckEvent> indexed_tree_check_emitter;
338 IndexedTreeCheck indexed_tree_check(
339 poseidon2, merkle_check, field_gt, DOM_SEP__WRITTEN_SLOTS_MERKLE, indexed_tree_check_emitter);
340 WrittenPublicDataSlotsTreeCheck written_public_data_slots_tree_check(indexed_tree_check,
342
343 EventEmitter<PublicDataTreeCheckEvent> public_data_tree_check_event_emitter;
344 PublicDataTreeCheck public_data_tree_check(
345 poseidon2, merkle_check, field_gt, execution_id_manager, public_data_tree_check_event_emitter);
346
347 // Attacker-controlled values
348 FF slot = 666;
349 AztecAddress contract_address = 0xDEADBEEF;
350 FF leaf_slot = unconstrained_compute_leaf_slot(contract_address, slot);
351 FF value = 999;
352
354 uint64_t low_leaf_index = 30;
355 std::vector<FF> low_leaf_sibling_path = { 1, 2, 3, 4, 5 };
356
357 AppendOnlyTreeSnapshot public_data_tree_before = AppendOnlyTreeSnapshot{
358 .root = 42,
359 .next_available_leaf_index = 128,
360 };
361 AppendOnlyTreeSnapshot written_slots_tree_before = written_public_data_slots_tree_check.get_snapshot();
362
363 EXPECT_CALL(poseidon2, hash(_)).WillRepeatedly([](const std::vector<FF>& inputs) {
365 });
366 EXPECT_CALL(field_gt, ff_gt(_, _)).WillRepeatedly([](const FF& a, const FF& b) {
367 return static_cast<uint256_t>(a) > static_cast<uint256_t>(b);
368 });
369 EXPECT_CALL(merkle_check, write)
370 .WillRepeatedly([]([[maybe_unused]] uint64_t domain_separator,
371 [[maybe_unused]] FF current_leaf,
372 FF new_leaf,
373 uint64_t leaf_index,
374 std::span<const FF> sibling_path,
375 [[maybe_unused]] FF prev_root) {
376 return unconstrained_root_from_path(DOM_SEP__WRITTEN_SLOTS_MERKLE, new_leaf, leaf_index, sibling_path);
377 });
378
379 // Generate cryptographically valid events via simulation (same as legitimate operation)
380 written_public_data_slots_tree_check.contains(contract_address, slot);
381 auto public_data_tree_after = public_data_tree_check.write(slot,
382 contract_address,
383 value,
384 low_leaf,
385 low_leaf_index,
386 low_leaf_sibling_path,
387 public_data_tree_before,
388 {},
389 false);
390 written_public_data_slots_tree_check.insert(contract_address, slot);
391 auto written_slots_tree_after = written_public_data_slots_tree_check.get_snapshot();
392
393 // Build trace with legitimate public_data_check rows
394 TestTraceContainer trace;
395 PublicDataTreeTraceBuilder public_data_tree_trace_builder;
396 public_data_tree_trace_builder.process(public_data_tree_check_event_emitter.dump_events(), trace);
397
398 IndexedTreeCheckTraceBuilder written_slots_tree_trace_builder;
399 written_slots_tree_trace_builder.process(indexed_tree_check_emitter.dump_events(), trace);
400
401 // Inject ghost sstore at row 0 where precomputed_idx matches public_data_check.clk.
402 // The mock execution_id_manager returns 0, so public_data_check.clk=0.
403 // Ghost row: sel_execute_sstore=0 but sel_write_public_data=1
404 trace.set(
405 0,
406 std::vector<std::pair<Column, FF>>{
407 { C::execution_clk, 0 },
408 { C::precomputed_first_row, 1 },
409 { C::execution_sel_execute_sstore, 0 },
410 { C::execution_sel_write_public_data, 1 },
411 { C::execution_contract_address, contract_address },
412 { C::execution_register_0_, value },
413 { C::execution_register_1_, slot },
414 { C::execution_sel_opcode_error, 0 },
415 { C::execution_discard, 0 },
416 { C::execution_prev_public_data_tree_root, public_data_tree_before.root },
417 { C::execution_prev_public_data_tree_size, public_data_tree_before.next_available_leaf_index },
418 { C::execution_public_data_tree_root, public_data_tree_after.root },
419 { C::execution_public_data_tree_size, public_data_tree_after.next_available_leaf_index },
420 { C::execution_prev_written_public_data_slots_tree_root, written_slots_tree_before.root },
421 { C::execution_prev_written_public_data_slots_tree_size,
422 written_slots_tree_before.next_available_leaf_index },
423 { C::execution_written_public_data_slots_tree_root, written_slots_tree_after.root },
424 { C::execution_written_public_data_slots_tree_size, written_slots_tree_after.next_available_leaf_index },
425 });
426
427 // The fix blocks ghost rows: sel_write_public_data = sel_execute_sstore * (1 - sel_opcode_error)
428 // When sel_execute_sstore=0 and sel_write_public_data=1: 1 * 1 = 1 != 0
429 EXPECT_THROW_WITH_MESSAGE(check_relation<sstore>(trace),
431}
432
433} // namespace
434} // namespace bb::avm2::constraining
#define EXPECT_THROW_WITH_MESSAGE(code, expectedMessageRegex)
Definition assert.hpp:224
FieldGreaterThan field_gt
MerkleCheck merkle_check
IndexedTreeCheck indexed_tree_check
static constexpr size_t SR_DYN_L2_GAS_IS_ZERO
Definition execution.hpp:52
static std::string get_subrelation_label(size_t index)
Definition execution.hpp:80
static constexpr size_t SR_SSTORE_WRITTEN_SLOTS_SIZE_NOT_CHANGED
Definition sstore.hpp:41
static constexpr size_t SR_OPCODE_ERROR_IF_OVERFLOW_OR_STATIC
Definition sstore.hpp:38
static constexpr size_t SR_SEL_WRITE_PUBLIC_DATA_IS_EXECUTE_AND_NOT_ERROR
Definition sstore.hpp:39
static constexpr size_t SR_SSTORE_MAX_DATA_WRITES_REACHED
Definition sstore.hpp:37
static constexpr size_t SR_SSTORE_WRITTEN_SLOTS_ROOT_NOT_CHANGED
Definition sstore.hpp:40
static constexpr size_t SR_SSTORE_PUBLIC_DATA_TREE_SIZE_NOT_CHANGED
Definition sstore.hpp:43
static std::string get_subrelation_label(size_t index)
Definition sstore.hpp:45
static constexpr size_t SR_SSTORE_PUBLIC_DATA_TREE_ROOT_NOT_CHANGED
Definition sstore.hpp:42
void set(Column col, uint32_t row, const FF &value, bool use_atomic_limbs=false)
static FF hash(const std::vector< FF > &input)
Hashes a vector of field elements.
ExecutionIdManager execution_id_manager
TestTraceContainer trace
FF a
FF b
IndexedTreeLeafData low_leaf
AvmProvingInputs inputs
void check_multipermutation_interaction(tracegen::TestTraceContainer &trace)
void check_interaction(tracegen::TestTraceContainer &trace)
TEST(AvmFixedVKTests, FixedVKCommitments)
Test that the fixed VK commitments agree with the ones computed from precomputed columns.
std::variant< PublicDataTreeReadWriteEvent, CheckPointEventType > PublicDataTreeCheckEvent
crypto::Poseidon2< crypto::Poseidon2Bn254ScalarFieldParams > poseidon2
IndexedLeaf< PublicDataLeafValue > PublicDataTreeLeafPreimage
std::variant< IndexedTreeReadWriteEvent, CheckPointEventType > IndexedTreeCheckEvent
::bb::crypto::merkle_tree::PublicDataLeafValue PublicDataLeafValue
Definition db.hpp:38
WrittenPublicDataSlotsTree build_public_data_slots_tree()
FF unconstrained_root_from_path(uint64_t domain_separator, const FF &leaf_value, const uint64_t leaf_index, std::span< const FF > path)
Definition merkle.cpp:12
FF unconstrained_compute_leaf_slot(const AztecAddress &contract_address, const FF &slot)
Definition merkle.cpp:30
lookup_settings< lookup_execution_check_written_storage_slot_settings_ > lookup_execution_check_written_storage_slot_settings
permutation_settings< perm_tx_balance_update_settings_ > perm_tx_balance_update_settings
Definition perms_tx.hpp:200
AvmFlavorSettings::FF FF
Definition field.hpp:10
permutation_settings< perm_sstore_storage_write_settings_ > perm_sstore_storage_write_settings
lookup_settings< lookup_sstore_record_written_storage_slot_settings_ > lookup_sstore_record_written_storage_slot_settings
void write(B &buf, field2< base_field, Params > const &value)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
constexpr field invert() const noexcept
NiceMock< MockExecution > execution
NiceMock< MockWrittenPublicDataSlotsTreeCheck > written_public_data_slots_tree_check