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