Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
opcode_spam.test.cpp
Go to the documentation of this file.
1#include <algorithm>
2#include <array>
3#include <cctype>
4#include <chrono>
5#include <cstdint>
6#include <cstdlib>
7#include <fstream>
8#include <optional>
9#include <string>
10#include <utility>
11#include <vector>
12
13#include <gtest/gtest.h>
14
27
28namespace bb::avm2 {
29namespace {
30
31using simulation::Instruction;
32using IB = testing::InstructionBuilder;
34using testing::PublicTxSimulationTester;
35using testing::TestEnqueuedCall;
36
37// A single opcode-spam scenario: a set of setup instructions, the target instruction(s) to spam,
38// optional cleanup (revert) instructions, and an optional per-tx limit that switches the scenario
39// to the two-contract (reverting nested call) pattern.
40struct SpamConfig {
41 std::string label;
46 // Whether the deployed contract's address should be passed as calldata[0] (used by the
47 // external-call spammer so it can call a contract whose address is only known at run time).
48 bool address_as_calldata = false;
49};
50
51// ---- Constants ----
52
53// Max bytecode size in bytes: bytecode is packed into fields (31 data bytes per field) with one
54// length field, so byteLength <= (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * 31.
55constexpr size_t BYTES_PER_FIELD = 31;
56constexpr size_t MAX_BYTECODE_BYTES = (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * BYTES_PER_FIELD;
57constexpr uint32_t MAX_U32 = 0xffffffff;
58
59// Warm tree entries: values inserted before running so that "warm" existence checks find them.
60const FF WARM_NOTE_HASH = FF(0xdeadbeefULL);
61const FF WARM_L1_TO_L2_MSG = FF(0xcafebabedeadbeefULL);
62const FF WARM_SILOED_NULLIFIER = FF(0xdeadbeef0001ULL);
63const FF WARM_STORAGE_SLOT = FF(0xdeadbeef0002ULL);
64const FF WARM_STORAGE_VALUE = FF(0xcafebabe0003ULL);
65// insert_warm_tree_entries appends to empty note-hash / l1-to-l2 trees, so the entries land at index 0.
66constexpr uint64_t WARM_NOTE_HASH_LEAF_INDEX = 0;
67constexpr uint64_t WARM_L1_TO_L2_MSG_LEAF_INDEX = 0;
68
69// Ordered so that limiting #configs per opcode still tests the max size (Field) first.
70const std::array<MemoryTag, 7> ALL_TAGS = {
72};
73const std::array<MemoryTag, 6> INT_TAGS = {
75};
76
77std::string tag_name(MemoryTag tag)
78{
79 switch (tag) {
80 case MemoryTag::FF:
81 return "FF";
82 case MemoryTag::U1:
83 return "U1";
84 case MemoryTag::U8:
85 return "U8";
86 case MemoryTag::U16:
87 return "U16";
88 case MemoryTag::U32:
89 return "U32";
90 case MemoryTag::U64:
91 return "U64";
92 case MemoryTag::U128:
93 return "U128";
94 }
95 return "?";
96}
97
98// ---- Deterministic value generation: a deterministic sequence so the scenarios are
99// reproducible. Always non-zero. ----
100FF next_value()
101{
102 static uint64_t counter = 0;
103 counter += 1;
104 return FF(uint256_t(0x9e3779b97f4a7c15ULL) * uint256_t(counter) + uint256_t(1));
105}
106
107FF truncate_to_tag(const FF& value, MemoryTag tag)
108{
109 const uint256_t v = static_cast<uint256_t>(value);
110 switch (tag) {
111 case MemoryTag::U1:
112 return FF(v & uint256_t(1));
113 case MemoryTag::U8:
114 return FF(v & uint256_t(0xffULL));
115 case MemoryTag::U16:
116 return FF(v & uint256_t(0xffffULL));
117 case MemoryTag::U32:
118 return FF(v & uint256_t(0xffffffffULL));
119 case MemoryTag::U64:
120 return FF(v & uint256_t(0xffffffffffffffffULL));
121 case MemoryTag::U128:
122 return FF(v & ((uint256_t(1) << 128) - 1));
123 case MemoryTag::FF:
124 return value;
125 }
126 return value;
127}
128
129// ---- Instruction factories ----
130
131// Smallest SET variant carrying `value` (truncated to `tag`) at `offset`, tagged with `tag`.
132Instruction set_mem(uint32_t offset, MemoryTag tag, const FF& value)
133{
134 const FF t = truncate_to_tag(value, tag);
135 const uint256_t v = static_cast<uint256_t>(t);
136 switch (tag) {
137 case MemoryTag::FF:
138 return IB(WireOpCode::SET_FF).operand<uint16_t>(static_cast<uint16_t>(offset)).operand(tag).operand(t).build();
139 case MemoryTag::U1:
140 case MemoryTag::U8:
141 return IB(WireOpCode::SET_8)
142 .operand<uint8_t>(static_cast<uint8_t>(offset))
143 .operand(tag)
144 .operand<uint8_t>(static_cast<uint8_t>(static_cast<uint64_t>(v)))
145 .build();
146 case MemoryTag::U16:
147 return IB(WireOpCode::SET_16)
148 .operand<uint16_t>(static_cast<uint16_t>(offset))
149 .operand(tag)
150 .operand<uint16_t>(static_cast<uint16_t>(static_cast<uint64_t>(v)))
151 .build();
152 case MemoryTag::U32:
153 return IB(WireOpCode::SET_32)
154 .operand<uint16_t>(static_cast<uint16_t>(offset))
155 .operand(tag)
156 .operand<uint32_t>(static_cast<uint32_t>(static_cast<uint64_t>(v)))
157 .build();
158 case MemoryTag::U64:
159 return IB(WireOpCode::SET_64)
160 .operand<uint16_t>(static_cast<uint16_t>(offset))
161 .operand(tag)
162 .operand<uint64_t>(static_cast<uint64_t>(v))
163 .build();
164 case MemoryTag::U128:
165 return IB(WireOpCode::SET_128)
166 .operand<uint16_t>(static_cast<uint16_t>(offset))
167 .operand(tag)
168 .operand<uint128_t>(static_cast<uint128_t>(v))
169 .build();
170 }
171 return IB(WireOpCode::SET_FF).operand<uint16_t>(static_cast<uint16_t>(offset)).operand(tag).operand(t).build();
172}
173
174Instruction set_u32(uint32_t offset, uint32_t value)
175{
176 return set_mem(offset, MemoryTag::U32, FF(value));
177}
178
179Instruction op3_8(WireOpCode op, uint8_t a, uint8_t b, uint8_t dst)
180{
181 return IB(op).operand(a).operand(b).operand(dst).build();
182}
183Instruction not8(uint8_t src, uint8_t dst)
184{
185 return IB(WireOpCode::NOT_8).operand(src).operand(dst).build();
186}
187Instruction cast8(uint8_t src, uint8_t dst, MemoryTag tag)
188{
189 return IB(WireOpCode::CAST_8).operand(src).operand(dst).operand(tag).build();
190}
191Instruction mov8(uint8_t src, uint8_t dst)
192{
193 return IB(WireOpCode::MOV_8).operand(src).operand(dst).build();
194}
195Instruction getenvvar(uint16_t dst, uint8_t var)
196{
197 return IB(WireOpCode::GETENVVAR_16).operand(dst).operand(var).build();
198}
199Instruction calldatacopy(uint16_t copy_size, uint16_t cd_start, uint16_t dst)
200{
201 return IB(WireOpCode::CALLDATACOPY).operand(copy_size).operand(cd_start).operand(dst).build();
202}
203Instruction successcopy(uint16_t dst)
204{
205 return IB(WireOpCode::SUCCESSCOPY).operand(dst).build();
206}
207Instruction returndatasize(uint16_t dst)
208{
209 return IB(WireOpCode::RETURNDATASIZE).operand(dst).build();
210}
211Instruction returndatacopy(uint16_t copy_size, uint16_t rd_start, uint16_t dst)
212{
213 return IB(WireOpCode::RETURNDATACOPY).operand(copy_size).operand(rd_start).operand(dst).build();
214}
215Instruction jump(uint32_t loc)
216{
217 return IB(WireOpCode::JUMP_32).operand(loc).build();
218}
219Instruction jumpi(uint16_t cond, uint32_t loc)
220{
221 return IB(WireOpCode::JUMPI_32).operand(cond).operand(loc).build();
222}
223Instruction internalcall(uint32_t loc)
224{
225 return IB(WireOpCode::INTERNALCALL).operand(loc).build();
226}
227Instruction internalreturn()
228{
229 return IB(WireOpCode::INTERNALRETURN).build();
230}
231Instruction sload(uint16_t slot, uint16_t addr, uint16_t dst)
232{
233 return IB(WireOpCode::SLOAD).operand(slot).operand(addr).operand(dst).build();
234}
235Instruction sstore(uint16_t src, uint16_t slot)
236{
237 return IB(WireOpCode::SSTORE).operand(src).operand(slot).build();
238}
239Instruction notehashexists(uint16_t nh, uint16_t leaf_idx, uint16_t exists)
240{
241 return IB(WireOpCode::NOTEHASHEXISTS).operand(nh).operand(leaf_idx).operand(exists).build();
242}
243Instruction nullifierexists(uint16_t siloed, uint16_t exists)
244{
245 return IB(WireOpCode::NULLIFIEREXISTS).operand(siloed).operand(exists).build();
246}
247Instruction l1tol2msgexists(uint16_t msg, uint16_t leaf_idx, uint16_t exists)
248{
249 return IB(WireOpCode::L1TOL2MSGEXISTS).operand(msg).operand(leaf_idx).operand(exists).build();
250}
251Instruction getcontractinstance(uint16_t addr, uint16_t dst, uint8_t member)
252{
253 return IB(WireOpCode::GETCONTRACTINSTANCE).operand(addr).operand(dst).operand(member).build();
254}
255Instruction emitnotehash(uint16_t nh)
256{
257 return IB(WireOpCode::EMITNOTEHASH).operand(nh).build();
258}
259Instruction emitnullifier(uint16_t n)
260{
261 return IB(WireOpCode::EMITNULLIFIER).operand(n).build();
262}
263Instruction sendl2tol1(uint16_t recipient, uint16_t content)
264{
265 return IB(WireOpCode::SENDL2TOL1MSG).operand(recipient).operand(content).build();
266}
267Instruction emitpubliclog(uint16_t log_size, uint16_t log_offset)
268{
269 return IB(WireOpCode::EMITPUBLICLOG).operand(log_size).operand(log_offset).build();
270}
271Instruction external_call(
272 WireOpCode op, uint16_t l2_gas, uint16_t da_gas, uint16_t addr, uint16_t args_size, uint16_t args)
273{
274 return IB(op).operand(l2_gas).operand(da_gas).operand(addr).operand(args_size).operand(args).build();
275}
276Instruction ret(uint16_t copy_size, uint16_t return_offset)
277{
278 return IB(WireOpCode::RETURN).operand(copy_size).operand(return_offset).build();
279}
280Instruction revert8(uint8_t ret_size, uint8_t return_offset)
281{
282 return IB(WireOpCode::REVERT_8).operand(ret_size).operand(return_offset).build();
283}
284Instruction poseidon2(uint16_t input, uint16_t output)
285{
286 return IB(WireOpCode::POSEIDON2PERM).operand(input).operand(output).build();
287}
288Instruction sha256compression(uint16_t out, uint16_t state, uint16_t inputs)
289{
290 return IB(WireOpCode::SHA256COMPRESSION).operand(out).operand(state).operand(inputs).build();
291}
292Instruction keccakf1600(uint16_t dst, uint16_t input)
293{
294 return IB(WireOpCode::KECCAKF1600).operand(dst).operand(input).build();
295}
296Instruction ecadd(uint16_t p1x, uint16_t p1y, uint16_t p2x, uint16_t p2y, uint16_t dst)
297{
298 return IB(WireOpCode::ECADD).operand(p1x).operand(p1y).operand(p2x).operand(p2y).operand(dst).build();
299}
300Instruction toradixbe(uint16_t src, uint16_t radix, uint16_t num_limbs, uint16_t out_bits, uint16_t dst)
301{
302 return IB(WireOpCode::TORADIXBE)
303 .operand(src)
304 .operand(radix)
305 .operand(num_limbs)
306 .operand(out_bits)
307 .operand(dst)
308 .build();
309}
310Instruction debuglog(uint16_t level, uint16_t message, uint16_t fields, uint16_t fields_size, uint16_t message_size)
311{
312 return IB(WireOpCode::DEBUGLOG)
313 .operand(level)
314 .operand(message)
315 .operand(fields)
316 .operand(fields_size)
317 .operand(message_size)
318 .build();
319}
320
321size_t instruction_byte_size(const Instruction& instruction)
322{
323 return instruction.serialize().size();
324}
325
326// ---- The external-call spam config (used directly for CALL/STATICCALL and as the outer contract
327// of the side-effect nested-call pattern). The contract address is forwarded via calldata[0]. ----
328//
329// Memory layout: [0]=const 0, [1]=const 1, [2]=const MAX_U32, [3]=call target address.
330SpamConfig external_call_config(WireOpCode call_opcode = WireOpCode::CALL)
331{
332 SpamConfig config;
333 config.label = "EXTERNAL_CALL";
334 config.setup = {
335 set_u32(0, 0), // cdStart / const 0
336 set_u32(1, 1), // copySize / argsSize / const 1
337 set_u32(2, MAX_U32), // l2Gas / daGas (capped to remaining gas by the AVM)
338 calldatacopy(/*copySize=*/1, /*cdStart=*/0, /*dst=*/3),
339 };
340 config.target = { external_call(call_opcode,
341 /*l2Gas=*/2,
342 /*daGas=*/2,
343 /*addr=*/3,
344 /*argsSize=*/1,
345 /*args=*/3) };
346 config.address_as_calldata = true;
347 return config;
348}
349
350// ---- Bytecode generators ----
351
352std::vector<uint8_t> create_opcode_spam_bytecode(const SpamConfig& config)
353{
354 BB_ASSERT(!config.limit.has_value(), "Standard spam bytecode requires a config without a limit");
355
356 std::vector<Instruction> instructions = config.setup;
357 const size_t setup_size = encode_to_bytecode(instructions).size();
358 const size_t target_size = encode_to_bytecode(config.target).size();
359 const size_t jump_size = instruction_byte_size(jump(0));
360
361 // Fill the remaining bytecode space with unrolled target instructions, then loop.
362 const size_t available_for_loop_body = MAX_BYTECODE_BYTES - setup_size - jump_size;
363 const size_t num_targets = available_for_loop_body / target_size;
364 const uint32_t loop_start_pc = static_cast<uint32_t>(setup_size);
365
366 instructions.reserve(instructions.size() + num_targets * config.target.size() + 1);
367 for (size_t i = 0; i < num_targets; ++i) {
368 instructions.insert(instructions.end(), config.target.begin(), config.target.end());
369 }
370 instructions.push_back(jump(loop_start_pc));
371 return encode_to_bytecode(instructions);
372}
373
374std::vector<uint8_t> create_side_effect_spam_bytecode(const SpamConfig& config)
375{
376 BB_ASSERT(config.limit.has_value(), "Side-effect spam bytecode requires a config with a limit");
377
378 std::vector<Instruction> instructions = config.setup;
379 for (uint32_t i = 0; i < *config.limit; ++i) {
380 instructions.insert(instructions.end(), config.target.begin(), config.target.end());
381 }
382 instructions.insert(instructions.end(), config.cleanup.begin(), config.cleanup.end());
383 return encode_to_bytecode(instructions);
384}
385
386// ---- World-state warm entries ----
387
388void insert_warm_tree_entries(PublicTxSimulationTester& tester, const AztecAddress& contract_address)
389{
390 tester.append_note_hash(WARM_NOTE_HASH);
391 tester.append_l1_to_l2_message(WARM_L1_TO_L2_MSG);
392 tester.insert_siloed_nullifier(WARM_SILOED_NULLIFIER);
393 tester.set_public_storage(contract_address, WARM_STORAGE_SLOT, WARM_STORAGE_VALUE);
394}
395
396// ---- Configuration table ----
397
398std::vector<SpamConfig> get_spam_configs()
399{
401
402 auto add = [&](SpamConfig config) { configs.push_back(std::move(config)); };
403
404 // Arithmetic / comparison / bitwise: one config per type tag.
405 auto binary_op = [&](WireOpCode op, const std::string& name, const std::array<MemoryTag, 7>& tags, uint8_t dst) {
406 for (auto tag : tags) {
407 add(SpamConfig{ .label = name + "/" + tag_name(tag),
408 .setup = { set_mem(0, tag, next_value()), set_mem(1, tag, next_value()) },
409 .target = { op3_8(op, 0, 1, dst) } });
410 }
411 };
412 auto binary_op_int = [&](WireOpCode op, const std::string& name, uint8_t dst, bool nonzero_b) {
413 for (auto tag : INT_TAGS) {
414 // For division the divisor must be non-zero after truncation; force its low bit.
415 const FF b = nonzero_b ? truncate_to_tag(FF(static_cast<uint256_t>(next_value()) | uint256_t(1)), tag)
416 : next_value();
417 add(SpamConfig{ .label = name + "/" + tag_name(tag),
418 .setup = { set_mem(0, tag, next_value()), set_mem(1, tag, b) },
419 .target = { op3_8(op, 0, 1, dst) } });
420 }
421 };
422
423 binary_op(WireOpCode::ADD_8, "ADD_8", ALL_TAGS, /*dst=*/0);
424 binary_op(WireOpCode::SUB_8, "SUB_8", ALL_TAGS, /*dst=*/0);
425 binary_op(WireOpCode::MUL_8, "MUL_8", ALL_TAGS, /*dst=*/0);
426 binary_op_int(WireOpCode::DIV_8, "DIV_8", /*dst=*/0, /*nonzero_b=*/true);
427 // FDIV (field only): non-zero divisor.
428 add(SpamConfig{ .label = "FDIV_8",
429 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_mem(1, MemoryTag::FF, next_value()) },
430 .target = { op3_8(WireOpCode::FDIV_8, 0, 1, 0) } });
431
432 binary_op(WireOpCode::EQ_8, "EQ_8", ALL_TAGS, /*dst=*/2);
433 binary_op(WireOpCode::LT_8, "LT_8", ALL_TAGS, /*dst=*/2);
434 binary_op(WireOpCode::LTE_8, "LTE_8", ALL_TAGS, /*dst=*/2);
435
436 binary_op_int(WireOpCode::AND_8, "AND_8", /*dst=*/0, /*nonzero_b=*/false);
437 binary_op_int(WireOpCode::OR_8, "OR_8", /*dst=*/0, /*nonzero_b=*/false);
438 binary_op_int(WireOpCode::XOR_8, "XOR_8", /*dst=*/0, /*nonzero_b=*/false);
439 for (auto tag : INT_TAGS) {
440 add(SpamConfig{ .label = std::string("NOT_8/") + tag_name(tag),
441 .setup = { set_mem(0, tag, next_value()) },
442 .target = { not8(0, 0) } });
443 }
444 // Shifts: shift amount of 1.
445 for (auto tag : INT_TAGS) {
446 add(SpamConfig{ .label = std::string("SHL_8/") + tag_name(tag),
447 .setup = { set_mem(0, tag, next_value()), set_mem(1, tag, FF(1)) },
448 .target = { op3_8(WireOpCode::SHL_8, 0, 1, 0) } });
449 }
450 for (auto tag : INT_TAGS) {
451 add(SpamConfig{ .label = std::string("SHR_8/") + tag_name(tag),
452 .setup = { set_mem(0, tag, next_value()), set_mem(1, tag, FF(1)) },
453 .target = { op3_8(WireOpCode::SHR_8, 0, 1, 0) } });
454 }
455
456 // CAST / MOV: one config per type tag.
457 for (auto tag : ALL_TAGS) {
458 add(SpamConfig{ .label = std::string("CAST_8/") + tag_name(tag),
459 .setup = { set_mem(0, tag, next_value()) },
460 .target = { cast8(0, 1, MemoryTag::U32) } });
461 }
462 for (auto tag : ALL_TAGS) {
463 add(SpamConfig{ .label = std::string("MOV_8/") + tag_name(tag),
464 .setup = { set_mem(0, tag, next_value()) },
465 .target = { mov8(0, 1) } });
466 }
467
468 // SET (only the 128-bit variant; others are equivalent in sim/proving cost).
469 add(SpamConfig{ .label = "SET_128",
470 .setup = {},
471 .target = { set_mem(0, MemoryTag::U128, FF(uint256_t(4242424242424242ULL))) } });
472
473 // Control flow.
474 add(SpamConfig{ .label = "JUMP_32", .setup = {}, .target = { jump(0) } });
475 add(SpamConfig{ .label = "JUMPI_32",
476 .setup = { set_mem(0, MemoryTag::U1, FF(0)) },
477 .target = { jumpi(/*cond=*/0, /*loc=*/0) } });
478 add(SpamConfig{ .label = "INTERNALCALL", .setup = {}, .target = { internalcall(/*loc=*/0) } });
479 // INTERNALCALL jumps to the INTERNALRETURN, which returns to the JUMP, which loops back to start.
480 const uint32_t internalcall_size = static_cast<uint32_t>(instruction_byte_size(internalcall(0)));
481 const uint32_t jump_size = static_cast<uint32_t>(instruction_byte_size(jump(0)));
482 add(SpamConfig{ .label = "INTERNALRETURN",
483 .setup = {},
484 .target = { internalcall(/*loc=*/internalcall_size + jump_size), jump(0), internalreturn() } });
485
486 // External calls (call self via calldata[0]).
487 add(external_call_config(WireOpCode::CALL));
488 {
489 SpamConfig staticcall = external_call_config(WireOpCode::STATICCALL);
490 staticcall.label = "STATICCALL";
491 add(std::move(staticcall));
492 }
493
494 // RETURN / REVERT terminate execution, so they use the side-effect (nested-call) pattern.
495 add(SpamConfig{ .label = "RETURN",
496 .setup = { set_u32(0, 0) },
497 .target = { ret(/*copySize=*/0, /*returnOffset=*/0) },
498 .limit = 1 });
499 add(SpamConfig{ .label = "REVERT_8",
500 .setup = { set_u32(0, 0) },
501 .target = { revert8(/*retSize=*/0, /*returnOffset=*/1) },
502 .limit = 1 });
503
504 // Environment.
505 add(SpamConfig{ .label = "GETENVVAR_16", .setup = {}, .target = { getenvvar(/*dst=*/0, /*var=*/0) } });
506
507 // CALLDATACOPY (dynamic gas with copy size).
508 add(SpamConfig{ .label = "CALLDATACOPY/Min copy size",
509 .setup = { set_u32(0, 0), set_u32(1, 0) },
510 .target = { calldatacopy(/*copySize=*/0, /*cdStart=*/1, /*dst=*/2) } });
511 add(SpamConfig{ .label = "CALLDATACOPY/Large copy size",
512 .setup = { set_u32(0, 1000), set_u32(1, 0) },
513 .target = { calldatacopy(/*copySize=*/0, /*cdStart=*/1, /*dst=*/2) } });
514 add(SpamConfig{ .label = "CALLDATACOPY/Near min copy size of 1",
515 .setup = { set_u32(0, 1), set_u32(1, 0) },
516 .target = { calldatacopy(/*copySize=*/0, /*cdStart=*/1, /*dst=*/2) } });
517
518 add(SpamConfig{ .label = "SUCCESSCOPY", .setup = {}, .target = { successcopy(/*dst=*/0) } });
519 add(SpamConfig{ .label = "RETURNDATASIZE", .setup = {}, .target = { returndatasize(/*dst=*/0) } });
520 add(SpamConfig{ .label = "RETURNDATACOPY/Min copy size",
521 .setup = { set_u32(0, 0), set_u32(1, 0) },
522 .target = { returndatacopy(/*copySize=*/0, /*rdStart=*/1, /*dst=*/2) } });
523 add(SpamConfig{ .label = "RETURNDATACOPY/Large copy size",
524 .setup = { set_u32(0, 1000), set_u32(1, 0) },
525 .target = { returndatacopy(/*copySize=*/0, /*rdStart=*/1, /*dst=*/2) } });
526 add(SpamConfig{ .label = "RETURNDATACOPY/Near min copy size of 1",
527 .setup = { set_u32(0, 1), set_u32(1, 0) },
528 .target = { returndatacopy(/*copySize=*/0, /*rdStart=*/1, /*dst=*/2) } });
529
530 // World-state reads.
531 add(SpamConfig{ .label = "SLOAD/Cold read (slot not written)",
532 .setup = { set_mem(0, MemoryTag::FF, next_value()), getenvvar(/*dst=*/1, /*var=*/0) },
533 .target = { sload(/*slot=*/0, /*addr=*/1, /*dst=*/2) } });
534 add(SpamConfig{ .label = "SLOAD/Warm read (from tree)",
535 .setup = { set_mem(0, MemoryTag::FF, WARM_STORAGE_SLOT), getenvvar(/*dst=*/1, /*var=*/0) },
536 .target = { sload(/*slot=*/0, /*addr=*/1, /*dst=*/2) } });
537 add(SpamConfig{ .label = "SLOAD/Warm read (SSTORE first, unique slot per SLOAD)",
538 .setup = { set_mem(0, MemoryTag::FF, next_value()),
539 set_mem(1, MemoryTag::FF, next_value()),
540 set_mem(2, MemoryTag::FF, FF(1)),
541 getenvvar(/*dst=*/3, /*var=*/0),
542 set_u32(4, 0) },
543 .target = { sstore(/*src=*/1, /*slot=*/0),
544 sload(/*slot=*/0, /*addr=*/3, /*dst=*/5),
545 op3_8(WireOpCode::ADD_8, 0, 2, 0) },
546 .cleanup = { revert8(/*retSize=*/4, /*returnOffset=*/0) },
547 .limit = MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX });
548
549 add(SpamConfig{ .label = "NOTEHASHEXISTS/Cold (non-existent)",
550 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_mem(1, MemoryTag::U64, next_value()) },
551 .target = { notehashexists(/*nh=*/0, /*leafIdx=*/1, /*exists=*/2) } });
552 add(SpamConfig{ .label = "NOTEHASHEXISTS/Warm (exists in tree)",
553 .setup = { set_mem(0, MemoryTag::FF, WARM_NOTE_HASH),
554 set_mem(1, MemoryTag::U64, FF(WARM_NOTE_HASH_LEAF_INDEX)) },
555 .target = { notehashexists(/*nh=*/0, /*leafIdx=*/1, /*exists=*/2) } });
556
557 add(SpamConfig{ .label = "NULLIFIEREXISTS/Non-existent nullifier",
558 .setup = { set_mem(0, MemoryTag::FF, next_value()) },
559 .target = { nullifierexists(/*siloed=*/0, /*exists=*/1) } });
560 add(SpamConfig{ .label = "NULLIFIEREXISTS/Existing nullifier (warm - from tree)",
561 .setup = { set_mem(0, MemoryTag::FF, WARM_SILOED_NULLIFIER) },
562 .target = { nullifierexists(/*siloed=*/0, /*exists=*/1) } });
563
564 add(SpamConfig{ .label = "L1TOL2MSGEXISTS/Cold (non-existent)",
565 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_mem(1, MemoryTag::U64, next_value()) },
566 .target = { l1tol2msgexists(/*msg=*/0, /*leafIdx=*/1, /*exists=*/2) } });
567 add(SpamConfig{ .label = "L1TOL2MSGEXISTS/Warm (exists in tree)",
568 .setup = { set_mem(0, MemoryTag::FF, WARM_L1_TO_L2_MSG),
569 set_mem(1, MemoryTag::U64, FF(WARM_L1_TO_L2_MSG_LEAF_INDEX)) },
570 .target = { l1tol2msgexists(/*msg=*/0, /*leafIdx=*/1, /*exists=*/2) } });
571
572 add(SpamConfig{ .label = "GETCONTRACTINSTANCE",
573 .setup = { getenvvar(/*dst=*/0, /*var=*/0) },
574 .target = { getcontractinstance(/*addr=*/0, /*dst=*/1, /*member=*/0) } });
575
576 // Side-effect limited opcodes (nested-call pattern).
577 add(SpamConfig{ .label = "EMITNOTEHASH",
578 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_u32(1, 0) },
579 .target = { emitnotehash(/*nh=*/0) },
580 .cleanup = { revert8(/*retSize=*/1, /*returnOffset=*/0) },
581 .limit = MAX_NOTE_HASHES_PER_TX });
582 add(SpamConfig{
583 .label = "EMITNULLIFIER",
584 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_mem(1, MemoryTag::FF, FF(1)), set_u32(2, 0) },
585 .target = { emitnullifier(/*n=*/0), op3_8(WireOpCode::ADD_8, 0, 1, 0) },
586 .cleanup = { revert8(/*retSize=*/2, /*returnOffset=*/0) },
587 .limit = MAX_NULLIFIERS_PER_TX - 1 });
588 add(SpamConfig{
589 .label = "SENDL2TOL1MSG",
590 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_mem(1, MemoryTag::FF, next_value()), set_u32(2, 0) },
591 .target = { sendl2tol1(/*recipient=*/0, /*content=*/1) },
592 .cleanup = { revert8(/*retSize=*/2, /*returnOffset=*/0) },
593 .limit = MAX_L2_TO_L1_MSGS_PER_TX });
594
595 // SSTORE: same-slot (no limit) and unique-slots (side-effect limited).
596 add(SpamConfig{
597 .label = "SSTORE/Same slot (no limit)",
598 .setup = { set_mem(0, MemoryTag::FF, next_value()), set_mem(1, MemoryTag::FF, next_value()), set_u32(2, 0) },
599 .target = { sstore(/*src=*/0, /*slot=*/1) } });
600 add(SpamConfig{ .label = "SSTORE/Unique slots (side-effect limited)",
601 .setup = { set_mem(0, MemoryTag::FF, next_value()),
602 set_mem(1, MemoryTag::FF, next_value()),
603 set_mem(2, MemoryTag::FF, FF(1)),
604 set_u32(3, 0) },
605 .target = { sstore(/*src=*/0, /*slot=*/1), op3_8(WireOpCode::ADD_8, 1, 2, 1) },
606 .cleanup = { revert8(/*retSize=*/3, /*returnOffset=*/0) },
607 .limit = MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX });
608
609 // EMITPUBLICLOG: many empty logs, and one max-size log.
610 add(SpamConfig{ .label = "EMITPUBLICLOG/Many empty logs, revert, repeat",
611 .setup = { set_u32(0, 0), set_u32(1, 0) },
612 .target = { emitpubliclog(/*logSize=*/0, /*logOffset=*/1) },
613 .cleanup = { revert8(/*retSize=*/1, /*returnOffset=*/0) },
614 .limit = FLAT_PUBLIC_LOGS_PAYLOAD_LENGTH / PUBLIC_LOG_HEADER_LENGTH });
615 add(SpamConfig{ .label = "EMITPUBLICLOG/One max size log, revert, repeat",
616 .setup = { set_u32(0, MAX_PUBLIC_LOG_SIZE_IN_FIELDS), set_u32(1, 0) },
617 .target = { emitpubliclog(/*logSize=*/0, /*logOffset=*/2) },
618 .cleanup = { revert8(/*retSize=*/1, /*returnOffset=*/0) },
619 .limit = 1 });
620
621 // Gadgets.
622 add(SpamConfig{ .label = "POSEIDON2PERM",
623 .setup = { set_mem(0, MemoryTag::FF, next_value()),
624 set_mem(1, MemoryTag::FF, next_value()),
625 set_mem(2, MemoryTag::FF, next_value()),
626 set_mem(3, MemoryTag::FF, next_value()) },
627 .target = { poseidon2(/*input=*/0, /*output=*/0) } });
628 {
630 for (uint16_t i = 0; i < 8; ++i) {
631 setup.push_back(set_mem(i, MemoryTag::U32, next_value()));
632 }
633 for (uint16_t i = 0; i < 16; ++i) {
634 setup.push_back(set_mem(static_cast<uint16_t>(8 + i), MemoryTag::U32, next_value()));
635 }
636 add(SpamConfig{ .label = "SHA256COMPRESSION",
637 .setup = std::move(setup),
638 .target = { sha256compression(/*out=*/0, /*state=*/0, /*inputs=*/8) } });
639 }
640 {
642 for (uint16_t i = 0; i < 25; ++i) {
643 setup.push_back(set_mem(i, MemoryTag::U64, next_value()));
644 }
645 add(SpamConfig{
646 .label = "KECCAKF1600", .setup = std::move(setup), .target = { keccakf1600(/*dst=*/0, /*input=*/0) } });
647 }
648 {
649 const FF gx = FF(grumpkin::g1::affine_one.x);
650 const FF gy = FF(grumpkin::g1::affine_one.y);
651 add(SpamConfig{ .label = "ECADD",
652 .setup = { set_mem(0, MemoryTag::FF, gx),
653 set_mem(1, MemoryTag::FF, gy),
654 set_mem(2, MemoryTag::FF, gx),
655 set_mem(3, MemoryTag::FF, gy) },
656 .target = { ecadd(/*p1x=*/0, /*p1y=*/1, /*p2x=*/2, /*p2y=*/3, /*dst=*/0) } });
657 }
658 add(SpamConfig{
659 .label = "TORADIXBE/Min limbs",
660 .setup = { set_mem(0, MemoryTag::FF, FF(1)), set_u32(1, 2), set_u32(2, 1), set_mem(3, MemoryTag::U1, FF(0)) },
661 .target = { toradixbe(/*src=*/0, /*radix=*/1, /*numLimbs=*/2, /*outBits=*/3, /*dst=*/4) } });
662 add(SpamConfig{ .label = "TORADIXBE/Max limbs",
663 .setup = { set_mem(0, MemoryTag::FF, next_value()),
664 set_u32(1, 2),
665 set_u32(2, 256),
666 set_mem(3, MemoryTag::U1, FF(0)) },
667 .target = { toradixbe(/*src=*/0, /*radix=*/1, /*numLimbs=*/2, /*outBits=*/3, /*dst=*/4) } });
668 add(SpamConfig{ .label = "TORADIXBE/Radix 3 (slow divmod path)",
669 .setup = { set_mem(0, MemoryTag::FF, next_value()),
670 set_u32(1, 3),
671 set_u32(2, 161),
672 set_mem(3, MemoryTag::U1, FF(0)) },
673 .target = { toradixbe(/*src=*/0, /*radix=*/1, /*numLimbs=*/2, /*outBits=*/3, /*dst=*/4) } });
674
675 // Misc.
676 add(SpamConfig{
677 .label = "DEBUGLOG",
678 .setup = { set_mem(0, MemoryTag::FF, FF(0)),
679 set_mem(1, MemoryTag::FF, FF(0)),
680 set_mem(2, MemoryTag::FF, FF(0)),
681 set_u32(3, 0) },
682 .target = { debuglog(/*level=*/0, /*message=*/1, /*fields=*/2, /*fieldsSize=*/3, /*messageSize=*/0) } });
683
684 return configs;
685}
686
687// ---- Runner ----
688
689// Deploys and simulates a single spam scenario end-to-end (handling the standard vs. side-effect
690// patterns). Returns the simulation result; the caller asserts the revert behavior and records metrics.
691// Pass a config with hint collection enabled to obtain proving inputs.
692TxSimulationResult run_opcode_spam_case(
693 PublicTxSimulationTester& tester,
694 const SpamConfig& config,
695 const PublicSimulatorConfig& sim_config = PublicTxSimulationTester::default_config())
696{
697 if (config.limit.has_value()) {
698 // Two-contract pattern: outer repeatedly calls inner, which spams the side effect then reverts.
699 const auto inner_bytecode = create_side_effect_spam_bytecode(config);
700 const auto outer_bytecode = create_opcode_spam_bytecode(external_call_config());
701 const auto inner = tester.deploy_contract(inner_bytecode);
702 const auto outer = tester.deploy_contract(outer_bytecode);
703 return tester.simulate_tx(
704 { TestEnqueuedCall{ .contract_address = outer.address, .calldata = { inner.address } } }, sim_config);
705 }
706
707 const auto bytecode = create_opcode_spam_bytecode(config);
708 const auto contract = tester.deploy_contract(bytecode);
709 insert_warm_tree_entries(tester, contract.address);
710
711 std::vector<FF> calldata;
712 if (config.address_as_calldata) {
713 calldata = { contract.address };
714 }
715 return tester.simulate_tx({ TestEnqueuedCall{ .contract_address = contract.address, .calldata = calldata } },
716 sim_config);
717}
718
719// ============================================================================
720// Benchmark / test
721// ============================================================================
722
723// One benchmark datapoint, matching the github-action-benchmark JSON shape consumed by the dashboard.
724struct BenchEntry {
725 std::string name;
726 double value;
727 std::string unit;
728};
729
730PublicSimulatorConfig proving_config()
731{
732 PublicSimulatorConfig config = PublicTxSimulationTester::default_config();
733 config.collect_hints = true;
734 config.collect_public_inputs = true;
735 return config;
736}
737
738std::string top_level_halting_message(const TxSimulationResult& result)
739{
740 if (result.call_stack_metadata.empty() || !result.call_stack_metadata[0].halting_message.has_value()) {
741 return "";
742 }
743 std::string message = *result.call_stack_metadata[0].halting_message;
744 std::transform(message.begin(), message.end(), message.begin(), [](unsigned char c) {
745 return static_cast<char>(std::tolower(c));
746 });
747 return message;
748}
749
750void write_bench_output(const std::vector<BenchEntry>& entries, const std::string& path)
751{
752 std::ofstream out(path);
753 out << "[\n";
754 for (size_t i = 0; i < entries.size(); ++i) {
755 out << " {\"name\": \"" << entries[i].name << "\", \"value\": " << entries[i].value << ", \"unit\": \""
756 << entries[i].unit << "\"}";
757 out << (i + 1 < entries.size() ? ",\n" : "\n");
758 }
759 out << "]\n";
760}
761
762// Spams every opcode through the full pipeline: fast simulation (until out of gas / explicit revert),
763// simulation with hint generation, and proving (check circuit). It records per-opcode gas and timing.
764// This is a heavy benchmark, so it is gated behind RUN_AVM_OPCODE_SPAM. Set BENCH_OUTPUT=<path> to emit
765// github-action-benchmark JSON.
766TEST(OpcodeSpam, SpamAllOpcodes)
767{
768 if (std::getenv("RUN_AVM_OPCODE_SPAM") == nullptr) {
769 GTEST_SKIP() << "Set RUN_AVM_OPCODE_SPAM=1 to run the opcode spam benchmark";
770 }
771
772 const auto configs = get_spam_configs();
774 entries.reserve(configs.size() * 3);
775
776 for (const auto& config : configs) {
777 SCOPED_TRACE(config.label);
778
779 // 1. Fast simulation: every scenario must halt (out of gas, or out of gas / explicit revert
780 // for the side-effect scenarios).
781 PublicTxSimulationTester tester;
782 const auto fast_sim_start = std::chrono::steady_clock::now();
783 const TxSimulationResult fast_result = run_opcode_spam_case(tester, config);
784 const auto fast_sim_end = std::chrono::steady_clock::now();
785 const double fast_sim_ms =
786 std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(fast_sim_end - fast_sim_start)
787 .count();
788
789 EXPECT_NE(fast_result.revert_code, RevertCode::OK) << "Opcode spam should halt for " << config.label;
790 const std::string reason = top_level_halting_message(fast_result);
791 if (!reason.empty()) {
792 const bool allowed =
793 reason.find("out of gas") != std::string::npos || reason.find("not enough") != std::string::npos;
794 EXPECT_TRUE(allowed) << "Unexpected top-level halt reason for " << config.label << ": " << reason;
795 }
796
797 // 2. Simulation for hint generation (on a fresh tester so the world state is clean).
798 PublicTxSimulationTester proving_tester;
799 const auto hint_sim_start = std::chrono::steady_clock::now();
800 const TxSimulationResult hint_result = run_opcode_spam_case(proving_tester, config, proving_config());
801 const auto hint_sim_end = std::chrono::steady_clock::now();
802 const double hint_sim_ms =
803 std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(hint_sim_end - hint_sim_start)
804 .count();
805 ASSERT_TRUE(hint_result.public_inputs.has_value());
806 ASSERT_TRUE(hint_result.hints.has_value());
807
808 const AvmProvingInputs proving_inputs{ .public_inputs = *hint_result.public_inputs,
809 .hints = *hint_result.hints };
810 AvmAPI api;
811 const auto prove_start = std::chrono::steady_clock::now();
812 const bool check_circuit_ok = api.check_circuit(proving_inputs);
813 const auto prove_end = std::chrono::steady_clock::now();
814 const double prove_ms =
815 std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(prove_end - prove_start).count();
816 EXPECT_TRUE(check_circuit_ok) << "check_circuit failed for " << config.label;
817
818 const double mana = static_cast<double>(fast_result.gas_used.public_gas.l2_gas);
819 entries.push_back({ "Opcode Spam/" + config.label + "/manaUsed", mana, "mana" });
820 entries.push_back({ "Opcode Spam/" + config.label + "/fastSimMs", fast_sim_ms, "ms" });
821 entries.push_back({ "Opcode Spam/" + config.label + "/hintSimMs", hint_sim_ms, "ms" });
822 entries.push_back({ "Opcode Spam/" + config.label + "/checkCircuitMs", prove_ms, "ms" });
823
824 info("Opcode Spam: ",
825 config.label,
826 " mana=",
827 mana,
828 " fastSim=",
829 fast_sim_ms,
830 "ms hintSim=",
831 hint_sim_ms,
832 "ms checkCircuit=",
833 prove_ms,
834 "ms");
835 }
836
837 if (const char* bench_output = std::getenv("BENCH_OUTPUT")) {
838 write_bench_output(entries, bench_output);
839 info("Wrote opcode spam benchmark output to ", bench_output);
840 }
841}
842
843} // namespace
844} // namespace bb::avm2
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
static constexpr affine_element affine_one
Definition group.hpp:50
#define info(...)
Definition log.hpp:93
FF a
FF b
std::string label
std::vector< uint8_t > bytecode
ssize_t offset
Definition engine.cpp:62
uint64_t da_gas
uint64_t l2_gas
Instruction instruction
AvmProvingInputs inputs
crypto::Poseidon2< crypto::Poseidon2Bn254ScalarFieldParams > poseidon2
std::vector< uint8_t > encode_to_bytecode(const std::vector< Instruction > &instructions)
AvmFlavorSettings::FF FF
Definition field.hpp:10
Instruction
Enumeration of VM instructions that can be executed.
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::vector< Instruction > cleanup
std::optional< uint32_t > limit
std::string unit
std::vector< Instruction > target
std::vector< Instruction > setup
std::string name
bool address_as_calldata
bb::VectorAffineElementPushSpan< BaseParams > out
unsigned __int128 uint128_t
Definition serialize.hpp:45
std::vector< MemoryValue > calldata
std::vector< uint8_t > serialize() const
VectorField result