13#include <gtest/gtest.h>
31using simulation::Instruction;
32using IB = testing::InstructionBuilder;
34using testing::PublicTxSimulationTester;
35using testing::TestEnqueuedCall;
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;
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);
66constexpr uint64_t WARM_NOTE_HASH_LEAF_INDEX = 0;
67constexpr uint64_t WARM_L1_TO_L2_MSG_LEAF_INDEX = 0;
102 static uint64_t counter = 0;
142 .operand<uint8_t>(
static_cast<uint8_t
>(
offset))
144 .operand<uint8_t>(
static_cast<uint8_t
>(
static_cast<uint64_t
>(v)))
148 .operand<uint16_t>(
static_cast<uint16_t
>(
offset))
150 .operand<uint16_t>(
static_cast<uint16_t
>(
static_cast<uint64_t
>(v)))
154 .operand<uint16_t>(
static_cast<uint16_t
>(
offset))
156 .operand<uint32_t>(
static_cast<uint32_t
>(
static_cast<uint64_t
>(v)))
160 .operand<uint16_t>(
static_cast<uint16_t
>(
offset))
162 .operand<uint64_t>(
static_cast<uint64_t
>(v))
166 .operand<uint16_t>(
static_cast<uint16_t
>(
offset))
181 return IB(op).operand(
a).operand(
b).operand(dst).build();
199Instruction calldatacopy(uint16_t copy_size, uint16_t cd_start, uint16_t dst)
211Instruction returndatacopy(uint16_t copy_size, uint16_t rd_start, uint16_t dst)
251Instruction getcontractinstance(uint16_t addr, uint16_t dst, uint8_t member)
263Instruction sendl2tol1(uint16_t recipient, uint16_t content)
267Instruction emitpubliclog(uint16_t log_size, uint16_t log_offset)
274 return IB(op).operand(
l2_gas).operand(
da_gas).operand(addr).operand(args_size).operand(args).build();
276Instruction ret(uint16_t copy_size, uint16_t return_offset)
280Instruction revert8(uint8_t ret_size, uint8_t return_offset)
292Instruction keccakf1600(uint16_t dst, uint16_t input)
296Instruction ecadd(uint16_t p1x, uint16_t p1y, uint16_t p2x, uint16_t p2y, uint16_t dst)
298 return IB(
WireOpCode::ECADD).operand(p1x).operand(p1y).operand(p2x).operand(p2y).operand(dst).build();
300Instruction toradixbe(uint16_t src, uint16_t radix, uint16_t num_limbs, uint16_t out_bits, uint16_t dst)
310Instruction debuglog(uint16_t level, uint16_t message, uint16_t fields, uint16_t fields_size, uint16_t
message_size)
316 .operand(fields_size)
333 config.label =
"EXTERNAL_CALL";
338 calldatacopy(1, 0, 3),
340 config.target = { external_call(call_opcode,
346 config.address_as_calldata =
true;
352std::vector<uint8_t> create_opcode_spam_bytecode(
const SpamConfig& config)
354 BB_ASSERT(!config.limit.has_value(),
"Standard spam bytecode requires a config without a limit");
359 const size_t jump_size = instruction_byte_size(jump(0));
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);
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());
370 instructions.push_back(jump(loop_start_pc));
374std::vector<uint8_t> create_side_effect_spam_bytecode(
const SpamConfig& config)
376 BB_ASSERT(config.limit.has_value(),
"Side-effect spam bytecode requires a config with a limit");
379 for (uint32_t i = 0; i < *config.limit; ++i) {
380 instructions.insert(instructions.end(), config.target.begin(), config.target.end());
382 instructions.insert(instructions.end(), config.cleanup.begin(), config.cleanup.end());
388void insert_warm_tree_entries(PublicTxSimulationTester& tester,
const AztecAddress& contract_address)
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);
402 auto add = [&](SpamConfig config) { configs.push_back(
std::move(config)); };
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) } });
412 auto binary_op_int = [&](
WireOpCode op,
const std::string&
name, uint8_t dst,
bool nonzero_b) {
413 for (
auto tag : INT_TAGS) {
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) } });
428 add(SpamConfig{ .label =
"FDIV_8",
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) } });
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)) },
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)) },
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()) },
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) } });
469 add(SpamConfig{ .label =
"SET_128",
474 add(SpamConfig{ .label =
"JUMP_32", .setup = {}, .target = { jump(0) } });
475 add(SpamConfig{ .label =
"JUMPI_32",
477 .target = { jumpi(0, 0) } });
478 add(SpamConfig{ .label =
"INTERNALCALL", .setup = {}, .target = { internalcall(0) } });
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",
484 .target = { internalcall(internalcall_size + jump_size), jump(0), internalreturn() } });
490 staticcall.label =
"STATICCALL";
495 add(SpamConfig{ .label =
"RETURN",
496 .setup = { set_u32(0, 0) },
497 .target = { ret(0, 0) },
499 add(SpamConfig{ .label =
"REVERT_8",
500 .setup = { set_u32(0, 0) },
501 .target = { revert8(0, 1) },
505 add(SpamConfig{ .label =
"GETENVVAR_16", .setup = {}, .target = { getenvvar(0, 0) } });
508 add(SpamConfig{ .label =
"CALLDATACOPY/Min copy size",
509 .setup = { set_u32(0, 0), set_u32(1, 0) },
510 .target = { calldatacopy(0, 1, 2) } });
511 add(SpamConfig{ .label =
"CALLDATACOPY/Large copy size",
512 .setup = { set_u32(0, 1000), set_u32(1, 0) },
513 .target = { calldatacopy(0, 1, 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(0, 1, 2) } });
518 add(SpamConfig{ .label =
"SUCCESSCOPY", .setup = {}, .target = { successcopy(0) } });
519 add(SpamConfig{ .label =
"RETURNDATASIZE", .setup = {}, .target = { returndatasize(0) } });
520 add(SpamConfig{ .label =
"RETURNDATACOPY/Min copy size",
521 .setup = { set_u32(0, 0), set_u32(1, 0) },
522 .target = { returndatacopy(0, 1, 2) } });
523 add(SpamConfig{ .label =
"RETURNDATACOPY/Large copy size",
524 .setup = { set_u32(0, 1000), set_u32(1, 0) },
525 .target = { returndatacopy(0, 1, 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(0, 1, 2) } });
531 add(SpamConfig{ .label =
"SLOAD/Cold read (slot not written)",
532 .setup = { set_mem(0,
MemoryTag::FF, next_value()), getenvvar(1, 0) },
533 .target = { sload(0, 1, 2) } });
534 add(SpamConfig{ .label =
"SLOAD/Warm read (from tree)",
535 .setup = { set_mem(0,
MemoryTag::FF, WARM_STORAGE_SLOT), getenvvar(1, 0) },
536 .target = { sload(0, 1, 2) } });
537 add(SpamConfig{ .label =
"SLOAD/Warm read (SSTORE first, unique slot per SLOAD)",
543 .target = { sstore(1, 0),
546 .cleanup = { revert8(4, 0) },
547 .limit = MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX });
549 add(SpamConfig{ .label =
"NOTEHASHEXISTS/Cold (non-existent)",
551 .target = { notehashexists(0, 1, 2) } });
552 add(SpamConfig{ .label =
"NOTEHASHEXISTS/Warm (exists in tree)",
555 .target = { notehashexists(0, 1, 2) } });
557 add(SpamConfig{ .label =
"NULLIFIEREXISTS/Non-existent nullifier",
559 .target = { nullifierexists(0, 1) } });
560 add(SpamConfig{ .label =
"NULLIFIEREXISTS/Existing nullifier (warm - from tree)",
561 .setup = { set_mem(0,
MemoryTag::FF, WARM_SILOED_NULLIFIER) },
562 .target = { nullifierexists(0, 1) } });
564 add(SpamConfig{ .label =
"L1TOL2MSGEXISTS/Cold (non-existent)",
566 .target = { l1tol2msgexists(0, 1, 2) } });
567 add(SpamConfig{ .label =
"L1TOL2MSGEXISTS/Warm (exists in tree)",
570 .target = { l1tol2msgexists(0, 1, 2) } });
572 add(SpamConfig{ .label =
"GETCONTRACTINSTANCE",
573 .setup = { getenvvar(0, 0) },
574 .target = { getcontractinstance(0, 1, 0) } });
577 add(SpamConfig{ .label =
"EMITNOTEHASH",
578 .setup = { set_mem(0,
MemoryTag::FF, next_value()), set_u32(1, 0) },
579 .target = { emitnotehash(0) },
580 .cleanup = { revert8(1, 0) },
581 .limit = MAX_NOTE_HASHES_PER_TX });
583 .label =
"EMITNULLIFIER",
586 .cleanup = { revert8(2, 0) },
587 .limit = MAX_NULLIFIERS_PER_TX - 1 });
589 .label =
"SENDL2TOL1MSG",
591 .target = { sendl2tol1(0, 1) },
592 .cleanup = { revert8(2, 0) },
593 .limit = MAX_L2_TO_L1_MSGS_PER_TX });
597 .label =
"SSTORE/Same slot (no limit)",
599 .target = { sstore(0, 1) } });
600 add(SpamConfig{ .label =
"SSTORE/Unique slots (side-effect limited)",
606 .cleanup = { revert8(3, 0) },
607 .limit = MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX });
610 add(SpamConfig{ .label =
"EMITPUBLICLOG/Many empty logs, revert, repeat",
611 .setup = { set_u32(0, 0), set_u32(1, 0) },
612 .target = { emitpubliclog(0, 1) },
613 .cleanup = { revert8(1, 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(0, 2) },
618 .cleanup = { revert8(1, 0) },
622 add(SpamConfig{ .label =
"POSEIDON2PERM",
630 for (uint16_t i = 0; i < 8; ++i) {
633 for (uint16_t i = 0; i < 16; ++i) {
636 add(SpamConfig{ .label =
"SHA256COMPRESSION",
638 .target = { sha256compression(0, 0, 8) } });
642 for (uint16_t i = 0; i < 25; ++i) {
646 .label =
"KECCAKF1600", .setup =
std::move(
setup), .target = { keccakf1600(0, 0) } });
651 add(SpamConfig{ .label =
"ECADD",
656 .target = { ecadd(0, 1, 2, 3, 0) } });
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(0, 1, 2, 3, 4) } });
662 add(SpamConfig{ .label =
"TORADIXBE/Max limbs",
667 .target = { toradixbe(0, 1, 2, 3, 4) } });
668 add(SpamConfig{ .label =
"TORADIXBE/Radix 3 (slow divmod path)",
673 .target = { toradixbe(0, 1, 2, 3, 4) } });
682 .target = { debuglog(0, 1, 2, 3, 0) } });
692TxSimulationResult run_opcode_spam_case(
693 PublicTxSimulationTester& tester,
694 const SpamConfig& config,
695 const PublicSimulatorConfig& sim_config = PublicTxSimulationTester::default_config())
697 if (config.limit.has_value()) {
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);
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);
712 if (config.address_as_calldata) {
715 return tester.simulate_tx({ TestEnqueuedCall{ .contract_address = contract.address, .calldata =
calldata } },
730PublicSimulatorConfig proving_config()
732 PublicSimulatorConfig config = PublicTxSimulationTester::default_config();
733 config.collect_hints =
true;
734 config.collect_public_inputs =
true;
738std::string top_level_halting_message(
const TxSimulationResult&
result)
740 if (
result.call_stack_metadata.empty() || !
result.call_stack_metadata[0].halting_message.has_value()) {
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));
752 std::ofstream
out(path);
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");
766TEST(OpcodeSpam, SpamAllOpcodes)
768 if (
std::getenv(
"RUN_AVM_OPCODE_SPAM") ==
nullptr) {
769 GTEST_SKIP() <<
"Set RUN_AVM_OPCODE_SPAM=1 to run the opcode spam benchmark";
772 const auto configs = get_spam_configs();
774 entries.reserve(configs.size() * 3);
776 for (
const auto& config : configs) {
777 SCOPED_TRACE(config.label);
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)
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()) {
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;
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)
805 ASSERT_TRUE(hint_result.public_inputs.has_value());
806 ASSERT_TRUE(hint_result.hints.has_value());
808 const AvmProvingInputs proving_inputs{ .public_inputs = *hint_result.public_inputs,
809 .hints = *hint_result.hints };
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;
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" });
824 info(
"Opcode Spam: ",
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);
#define BB_ASSERT(expression,...)
static constexpr affine_element affine_one
std::vector< uint8_t > bytecode
crypto::Poseidon2< crypto::Poseidon2Bn254ScalarFieldParams > poseidon2
std::vector< uint8_t > encode_to_bytecode(const std::vector< Instruction > &instructions)
Instruction
Enumeration of VM instructions that can be executed.
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
std::vector< Instruction > cleanup
std::optional< uint32_t > limit
std::vector< Instruction > target
std::vector< Instruction > setup
bb::VectorAffineElementPushSpan< BaseParams > out
unsigned __int128 uint128_t
std::vector< MemoryValue > calldata
std::vector< uint8_t > serialize() const