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
29
30namespace bb::avm2::constraining {
31namespace {
32
33using tracegen::ExecutionTraceBuilder;
34using tracegen::IndexedTreeCheckTraceBuilder;
35using tracegen::PublicDataTreeTraceBuilder;
36using tracegen::TestTraceContainer;
37
39using simulation::EventEmitter;
40using simulation::IndexedTreeCheck;
42using simulation::MockExecutionIdManager;
43using simulation::MockFieldGreaterThan;
44using simulation::MockMerkleCheck;
45using simulation::MockPoseidon2;
46using simulation::PublicDataTreeCheck;
51using simulation::WrittenPublicDataSlotsTreeCheck;
52
53using testing::_;
54using testing::NiceMock;
55
57using C = Column;
58using sstore = bb::avm2::sstore<FF>;
61
62TEST(SStoreConstrainingTest, PositiveTest)
63{
64 TestTraceContainer trace({
65 { { C::execution_sel_execute_sstore, 1 },
66 { C::execution_sel_gas_sstore, 1 },
67 { C::execution_dynamic_da_gas_factor, 1 },
68 { C::execution_register_0_, /*value=*/27 },
69 { C::execution_register_1_, /*slot=*/42 },
70 { C::execution_prev_written_public_data_slots_tree_size, 5 },
71 { C::execution_max_data_writes_reached, 0 },
72 { C::execution_remaining_data_writes_inv,
74 { C::execution_sel_write_public_data, 1 },
75 { C::execution_subtrace_operation_id, AVM_EXEC_OP_ID_SSTORE } },
76 });
77 check_relation<sstore>(trace);
78}
79
80TEST(SStoreConstrainingTest, NegativeDynamicL2GasIsZero)
81{
82 TestTraceContainer trace({ {
83 { C::execution_sel_execute_sstore, 1 },
84 { C::execution_dynamic_l2_gas_factor, 1 },
85 } });
86 EXPECT_THROW_WITH_MESSAGE(check_relation<execution>(trace, execution::SR_DYN_L2_GAS_IS_ZERO), "DYN_L2_GAS_IS_ZERO");
87}
88
89TEST(SStoreConstrainingTest, MaxDataWritesReached)
90{
91 TestTraceContainer trace({
92 {
93 { C::execution_sel_execute_sstore, 1 },
94 { C::execution_prev_written_public_data_slots_tree_size,
96 { C::execution_remaining_data_writes_inv, 0 },
97 { C::execution_max_data_writes_reached, 1 },
98 },
99 });
100 check_relation<sstore>(trace, sstore::SR_SSTORE_MAX_DATA_WRITES_REACHED);
101
102 trace.set(C::execution_max_data_writes_reached, 0, 0);
103
105 "SSTORE_MAX_DATA_WRITES_REACHED");
106}
107
108TEST(SStoreConstrainingTest, OpcodeError)
109{
110 TestTraceContainer trace({
111 {
112 { C::execution_sel_execute_sstore, 1 },
113 { C::execution_dynamic_da_gas_factor, 1 },
114 { C::execution_max_data_writes_reached, 1 },
115 { C::execution_sel_opcode_error, 1 },
116 },
117 {
118 { C::execution_sel_execute_sstore, 1 },
119 { C::execution_dynamic_da_gas_factor, 0 },
120 { C::execution_max_data_writes_reached, 0 },
121 { C::execution_is_static, 1 },
122 { C::execution_sel_opcode_error, 1 },
123 },
124 {
125 { C::execution_sel_execute_sstore, 1 },
126 { C::execution_dynamic_da_gas_factor, 0 },
127 { C::execution_max_data_writes_reached, 1 },
128 { C::execution_sel_opcode_error, 0 },
129 },
130 });
131 check_relation<sstore>(trace, sstore::SR_OPCODE_ERROR_IF_OVERFLOW_OR_STATIC);
132
133 trace.set(C::execution_dynamic_da_gas_factor, 0, 0);
134
136 "OPCODE_ERROR_IF_OVERFLOW_OR_STATIC");
137
138 trace.set(C::execution_dynamic_da_gas_factor, 0, 1);
139
140 trace.set(C::execution_is_static, 1, 0);
141
143 "OPCODE_ERROR_IF_OVERFLOW_OR_STATIC");
144}
145
146TEST(SStoreConstrainingTest, TreeStateNotChangedOnError)
147{
148 TestTraceContainer trace({ {
149 { C::execution_sel_execute_sstore, 1 },
150 { C::execution_prev_public_data_tree_root, 27 },
151 { C::execution_prev_public_data_tree_size, 5 },
152 { C::execution_prev_written_public_data_slots_tree_root, 28 },
153 { C::execution_prev_written_public_data_slots_tree_size, 6 },
154 { C::execution_public_data_tree_root, 27 },
155 { C::execution_public_data_tree_size, 5 },
156 { C::execution_written_public_data_slots_tree_root, 28 },
157 { C::execution_written_public_data_slots_tree_size, 6 },
158 { C::execution_sel_opcode_error, 1 },
159 } });
160
161 check_relation<sstore>(trace,
166
167 // Negative test: written slots tree root must be the same
168 trace.set(C::execution_written_public_data_slots_tree_root, 0, 29);
170 "SSTORE_WRITTEN_SLOTS_ROOT_NOT_CHANGED");
171
172 // Negative test: written slots tree size must be the same
173 trace.set(C::execution_written_public_data_slots_tree_size, 0, 7);
175 "SSTORE_WRITTEN_SLOTS_SIZE_NOT_CHANGED");
176
177 // Negative test: public data tree root must be the same
178 trace.set(C::execution_public_data_tree_root, 0, 29);
180 "SSTORE_PUBLIC_DATA_TREE_ROOT_NOT_CHANGED");
181
182 // Negative test: public data tree size must be the same
183 trace.set(C::execution_public_data_tree_size, 0, 7);
185 "SSTORE_PUBLIC_DATA_TREE_SIZE_NOT_CHANGED");
186}
187
188// Test that ghost rows (sel_execute_sstore=0) cannot set sel_write_public_data=1
189// This verifies the fix: sel_write_public_data * (1 - sel_execute_sstore) = 0
190TEST(SStoreConstrainingTest, NegativeGhostRowStorageWrite_RelationsOnly)
191{
192 // Try to create a ghost row (sel_execute_sstore=0) with sel_write_public_data=1
193 TestTraceContainer trace({
194 {
195 { C::execution_sel_execute_sstore, 0 }, // Ghost row: sstore not executing
196 { C::execution_sel_write_public_data, 1 }, // Try to fire storage write anyway
197 { C::execution_register_0_, /*value=*/999 }, // Arbitrary value
198 { C::execution_register_1_, /*slot=*/666 }, // Arbitrary slot
199 { C::execution_contract_address, 0xDEADBEEF }, // Arbitrary address
200 { C::execution_sel_opcode_error, 0 },
201 },
202 });
203
204 // The fix: sel_write_public_data = sel_execute_sstore * (1 - sel_opcode_error)
205 // When sel_execute_sstore=0 and sel_write_public_data=1: 1 * (1-0) = 1 != 0 -> FAILS
206 EXPECT_THROW_WITH_MESSAGE(check_relation<sstore>(trace), "SEL_WRITE_PUBLIC_DATA_IS_EXECUTE_AND_NOT_ERROR");
207}
208
209TEST(SStoreConstrainingTest, Interactions)
210{
211 NiceMock<MockPoseidon2> poseidon2;
212 NiceMock<MockFieldGreaterThan> field_gt;
213 NiceMock<MockMerkleCheck> merkle_check;
214 NiceMock<MockExecutionIdManager> execution_id_manager;
215
216 EventEmitter<IndexedTreeCheckEvent> indexed_tree_check_emitter;
217 IndexedTreeCheck indexed_tree_check(poseidon2, merkle_check, field_gt, indexed_tree_check_emitter);
218
219 WrittenPublicDataSlotsTreeCheck written_public_data_slots_tree_check(indexed_tree_check,
221
222 EventEmitter<PublicDataTreeCheckEvent> public_data_tree_check_event_emitter;
223 PublicDataTreeCheck public_data_tree_check(
224 poseidon2, merkle_check, field_gt, execution_id_manager, public_data_tree_check_event_emitter);
225
226 FF slot = 42;
227 AztecAddress contract_address = 1;
228 FF leaf_slot = unconstrained_compute_leaf_slot(contract_address, slot);
229 FF value = 27;
230
232 uint64_t low_leaf_index = 30;
233 std::vector<FF> low_leaf_sibling_path = { 1, 2, 3, 4, 5 };
234
235 AppendOnlyTreeSnapshot public_data_tree_before = AppendOnlyTreeSnapshot{
236 .root = 42,
237 .next_available_leaf_index = 128,
238 };
239 AppendOnlyTreeSnapshot written_slots_tree_before = written_public_data_slots_tree_check.get_snapshot();
240
241 EXPECT_CALL(poseidon2, hash(_)).WillRepeatedly([](const std::vector<FF>& inputs) {
243 });
244 EXPECT_CALL(field_gt, ff_gt(_, _)).WillRepeatedly([](const FF& a, const FF& b) {
245 return static_cast<uint256_t>(a) > static_cast<uint256_t>(b);
246 });
247
248 EXPECT_CALL(merkle_check, write)
249 .WillRepeatedly([]([[maybe_unused]] FF current_leaf,
250 FF new_leaf,
251 uint64_t leaf_index,
252 std::span<const FF> sibling_path,
253 [[maybe_unused]] FF prev_root) {
254 return unconstrained_root_from_path(new_leaf, leaf_index, sibling_path);
255 });
256
257 written_public_data_slots_tree_check.contains(contract_address, slot);
258
259 auto public_data_tree_after = public_data_tree_check.write(slot,
260 contract_address,
261 value,
262 low_leaf,
263 low_leaf_index,
264 low_leaf_sibling_path,
265 public_data_tree_before,
266 {},
267 false);
268 written_public_data_slots_tree_check.insert(contract_address, slot);
269 auto written_slots_tree_after = written_public_data_slots_tree_check.get_snapshot();
270
271 TestTraceContainer trace({
272 {
273 { C::execution_sel_execute_sstore, 1 },
274 { C::execution_contract_address, contract_address },
275 { C::execution_sel_gas_sstore, 1 },
276 { C::execution_written_slots_tree_height, AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_HEIGHT },
277 { C::execution_written_slots_tree_siloing_separator, DOM_SEP__PUBLIC_LEAF_SLOT },
278 { C::execution_dynamic_da_gas_factor, 1 },
279 { C::execution_register_0_, value },
280 { C::execution_register_1_, slot },
281 { C::execution_max_data_writes_reached, 0 },
282 { C::execution_remaining_data_writes_inv,
284 written_slots_tree_before.next_available_leaf_index)
285 .invert() },
286 { C::execution_subtrace_operation_id, AVM_EXEC_OP_ID_SSTORE },
287 { C::execution_sel_write_public_data, 1 },
288 { C::execution_prev_public_data_tree_root, public_data_tree_before.root },
289 { C::execution_prev_public_data_tree_size, public_data_tree_before.next_available_leaf_index },
290 { C::execution_public_data_tree_root, public_data_tree_after.root },
291 { C::execution_public_data_tree_size, public_data_tree_after.next_available_leaf_index },
292 { C::execution_prev_written_public_data_slots_tree_root, written_slots_tree_before.root },
293 { C::execution_prev_written_public_data_slots_tree_size,
294 written_slots_tree_before.next_available_leaf_index },
295 { C::execution_written_public_data_slots_tree_root, written_slots_tree_after.root },
296 { C::execution_written_public_data_slots_tree_size, written_slots_tree_after.next_available_leaf_index },
297 },
298 });
299
300 PublicDataTreeTraceBuilder public_data_tree_trace_builder;
301 public_data_tree_trace_builder.process(public_data_tree_check_event_emitter.dump_events(), trace);
302
303 IndexedTreeCheckTraceBuilder written_slots_tree_trace_builder;
304 written_slots_tree_trace_builder.process(indexed_tree_check_emitter.dump_events(), trace);
305
306 check_relation<sstore>(trace);
307 check_interaction<ExecutionTraceBuilder,
310 check_multipermutation_interaction<PublicDataTreeTraceBuilder,
313}
314
315// Ghost row injection attack test.
316// Verifies that the fix (sel_write_public_data * (1 - sel_execute_sstore) = 0) prevents
317// a malicious prover from injecting arbitrary storage writes via ghost sstore rows.
318//
319// Attack vector (now blocked):
320// 1. Create ghost sstore row (sel_execute_sstore=0, sel_write_public_data=1)
321// 2. Populate public_data_check trace with legitimate rows via simulation
322// 3. Align clk values so the STORAGE_WRITE permutation matches
323// 4. Without the fix, the permutation would pass and arbitrary writes would be possible
324TEST(SStoreConstrainingTest, NegativeFullAttackWithAllTraces)
325{
326 NiceMock<MockPoseidon2> poseidon2;
327 NiceMock<MockFieldGreaterThan> field_gt;
328 NiceMock<MockMerkleCheck> merkle_check;
329 NiceMock<MockExecutionIdManager> execution_id_manager;
330
331 EventEmitter<IndexedTreeCheckEvent> indexed_tree_check_emitter;
332 IndexedTreeCheck indexed_tree_check(poseidon2, merkle_check, field_gt, indexed_tree_check_emitter);
333 WrittenPublicDataSlotsTreeCheck written_public_data_slots_tree_check(indexed_tree_check,
335
336 EventEmitter<PublicDataTreeCheckEvent> public_data_tree_check_event_emitter;
337 PublicDataTreeCheck public_data_tree_check(
338 poseidon2, merkle_check, field_gt, execution_id_manager, public_data_tree_check_event_emitter);
339
340 // Attacker-controlled values
341 FF slot = 666;
342 AztecAddress contract_address = 0xDEADBEEF;
343 FF leaf_slot = unconstrained_compute_leaf_slot(contract_address, slot);
344 FF value = 999;
345
347 uint64_t low_leaf_index = 30;
348 std::vector<FF> low_leaf_sibling_path = { 1, 2, 3, 4, 5 };
349
350 AppendOnlyTreeSnapshot public_data_tree_before = AppendOnlyTreeSnapshot{
351 .root = 42,
352 .next_available_leaf_index = 128,
353 };
354 AppendOnlyTreeSnapshot written_slots_tree_before = written_public_data_slots_tree_check.get_snapshot();
355
356 EXPECT_CALL(poseidon2, hash(_)).WillRepeatedly([](const std::vector<FF>& inputs) {
358 });
359 EXPECT_CALL(field_gt, ff_gt(_, _)).WillRepeatedly([](const FF& a, const FF& b) {
360 return static_cast<uint256_t>(a) > static_cast<uint256_t>(b);
361 });
362 EXPECT_CALL(merkle_check, write)
363 .WillRepeatedly([]([[maybe_unused]] FF current_leaf,
364 FF new_leaf,
365 uint64_t leaf_index,
366 std::span<const FF> sibling_path,
367 [[maybe_unused]] FF prev_root) {
368 return unconstrained_root_from_path(new_leaf, leaf_index, sibling_path);
369 });
370
371 // Generate cryptographically valid events via simulation (same as legitimate operation)
372 written_public_data_slots_tree_check.contains(contract_address, slot);
373 auto public_data_tree_after = public_data_tree_check.write(slot,
374 contract_address,
375 value,
376 low_leaf,
377 low_leaf_index,
378 low_leaf_sibling_path,
379 public_data_tree_before,
380 {},
381 false);
382 written_public_data_slots_tree_check.insert(contract_address, slot);
383 auto written_slots_tree_after = written_public_data_slots_tree_check.get_snapshot();
384
385 // Build trace with legitimate public_data_check rows
386 TestTraceContainer trace;
387 PublicDataTreeTraceBuilder public_data_tree_trace_builder;
388 public_data_tree_trace_builder.process(public_data_tree_check_event_emitter.dump_events(), trace);
389
390 IndexedTreeCheckTraceBuilder written_slots_tree_trace_builder;
391 written_slots_tree_trace_builder.process(indexed_tree_check_emitter.dump_events(), trace);
392
393 // Inject ghost sstore at row 0 where precomputed_idx matches public_data_check.clk.
394 // The mock execution_id_manager returns 0, so public_data_check.clk=0.
395 // Ghost row: sel_execute_sstore=0 but sel_write_public_data=1
396 trace.set(
397 0,
398 std::vector<std::pair<Column, FF>>{
399 { C::execution_clk, 0 },
400 { C::precomputed_first_row, 1 },
401 { C::execution_sel_execute_sstore, 0 },
402 { C::execution_sel_write_public_data, 1 },
403 { C::execution_contract_address, contract_address },
404 { C::execution_register_0_, value },
405 { C::execution_register_1_, slot },
406 { C::execution_sel_opcode_error, 0 },
407 { C::execution_discard, 0 },
408 { C::execution_prev_public_data_tree_root, public_data_tree_before.root },
409 { C::execution_prev_public_data_tree_size, public_data_tree_before.next_available_leaf_index },
410 { C::execution_public_data_tree_root, public_data_tree_after.root },
411 { C::execution_public_data_tree_size, public_data_tree_after.next_available_leaf_index },
412 { C::execution_prev_written_public_data_slots_tree_root, written_slots_tree_before.root },
413 { C::execution_prev_written_public_data_slots_tree_size,
414 written_slots_tree_before.next_available_leaf_index },
415 { C::execution_written_public_data_slots_tree_root, written_slots_tree_after.root },
416 { C::execution_written_public_data_slots_tree_size, written_slots_tree_after.next_available_leaf_index },
417 });
418
419 // The fix blocks ghost rows: sel_write_public_data = sel_execute_sstore * (1 - sel_opcode_error)
420 // When sel_execute_sstore=0 and sel_write_public_data=1: 1 * 1 = 1 != 0
421 EXPECT_THROW_WITH_MESSAGE(check_relation<sstore>(trace), "SEL_WRITE_PUBLIC_DATA_IS_EXECUTE_AND_NOT_ERROR");
422}
423
424} // namespace
425} // namespace bb::avm2::constraining
#define EXPECT_THROW_WITH_MESSAGE(code, expectedMessageRegex)
Definition assert.hpp:193
#define AVM_EXEC_OP_ID_SSTORE
#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_HEIGHT
#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE
#define DOM_SEP__PUBLIC_LEAF_SLOT
#define MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX
FieldGreaterThan field_gt
MerkleCheck merkle_check
IndexedTreeCheck indexed_tree_check
static constexpr size_t SR_DYN_L2_GAS_IS_ZERO
Definition execution.hpp:53
static constexpr size_t SR_SSTORE_WRITTEN_SLOTS_SIZE_NOT_CHANGED
Definition sstore.hpp:42
static constexpr size_t SR_OPCODE_ERROR_IF_OVERFLOW_OR_STATIC
Definition sstore.hpp:39
static constexpr size_t SR_SSTORE_MAX_DATA_WRITES_REACHED
Definition sstore.hpp:38
static constexpr size_t SR_SSTORE_WRITTEN_SLOTS_ROOT_NOT_CHANGED
Definition sstore.hpp:41
static constexpr size_t SR_SSTORE_PUBLIC_DATA_TREE_SIZE_NOT_CHANGED
Definition sstore.hpp:44
static constexpr size_t SR_SSTORE_PUBLIC_DATA_TREE_ROOT_NOT_CHANGED
Definition sstore.hpp:43
void set(Column col, uint32_t row, const FF &value)
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
FF unconstrained_root_from_path(const FF &leaf_value, const uint64_t leaf_index, std::span< const FF > path)
Definition merkle.cpp:12
std::variant< IndexedTreeReadWriteEvent, CheckPointEventType > IndexedTreeCheckEvent
::bb::crypto::merkle_tree::PublicDataLeafValue PublicDataLeafValue
Definition db.hpp:38
WrittenPublicDataSlotsTree build_public_data_slots_tree()
FF unconstrained_compute_leaf_slot(const AztecAddress &contract_address, const FF &slot)
Definition merkle.cpp:26
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