-
Notifications
You must be signed in to change notification settings - Fork 578
test: AVM opcode spammer #18919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: AVM opcode spammer #18919
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
254bdd6 to
877b465
Compare
| const fnSelector = await getFunctionSelector(call.fnName, contractArtifact); | ||
| const fnAbi = getContractFunctionAbi(call.fnName, contractArtifact)!; | ||
| const encodedArgs = encodeArguments(fnAbi, call.args); | ||
| const calldata = [fnSelector.toField(), ...encodedArgs]; | ||
| let calldata: Fr[] = []; | ||
| if (!call.fnName) { | ||
| this.logger.warn( | ||
| `No function name specified for call to contract ${call.address.toString()}. Assuming this is a custom bytecode with no public_dispatch function.`, | ||
| ); | ||
| this.logger.warn(`Not using ABI to encode arguments. Not prepending fn selector to calldata.`); | ||
| try { | ||
| calldata = call.args.map(arg => new Fr(arg)); | ||
| } catch (error) { | ||
| this.logger.warn(`Tried assuming that all arguments are Field-like. Failed. Error: ${error}`); | ||
| throw error; | ||
| } | ||
| } else { | ||
| const fnSelector = await getFunctionSelector(call.fnName, contractArtifact); | ||
| const fnAbi = getContractFunctionAbi(call.fnName, contractArtifact)!; | ||
| const encodedArgs = encodeArguments(fnAbi, call.args); | ||
| calldata = [fnSelector.toField(), ...encodedArgs]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tester was dying if fnName wasn't there. And it shouldn't need it!
68f780c to
ded0bc8
Compare
3877161 to
c576708
Compare
…ferent variants per opcode
…at do not use fn selector
e3c62c6 to
d639418
Compare
d639418 to
829981d
Compare
| const selector = FunctionSelector.fromField(calldata[0]); | ||
| return (await db.getDebugFunctionName(contractAddress, selector)) ?? selector.toString(); | ||
| const fallbackName = `<calldata[0]:${calldata[0].toString()}> (Contract Address: ${contractAddress})`; | ||
| const selector = FunctionSelector.fromFieldOrUndefined(calldata[0]); | ||
| if (!selector) { | ||
| return fallbackName; | ||
| } | ||
| return (await db.getDebugFunctionName(contractAddress, selector)) ?? fallbackName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was exploding if calldata[0] was there but couldn't be interpreted as a function selector (4 bytes)
829981d to
e421f5f
Compare
e421f5f to
bc5263d
Compare
| /*globals=*/ undefined, // default | ||
| /*metrics=*/ undefined, | ||
| /*useCppSimulator=*/ true, | ||
| simConfig, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this using TS before? How was it working?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was using CppVsTs (sim via both and compare). And it was working because the CppVsTs simulator returns TS simulation's results which include hints and PIs.
This PR changes CppVsTs simulator to return C++'s results instead, so this broke until I specified collectHints and PIs in this config. Now that CppVsTs returns C++'s results, it respects the sim config
yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts
Outdated
Show resolved
Hide resolved
| const COLLECT_META_CHECK_RET = true; | ||
| const expectToBeTrue = COLLECT_META_CHECK_RET ? (x: boolean) => expect(x).toBe(true) : () => {}; | ||
|
|
||
| describe('Opcode Spammer Benchmarks', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we disable this in CI as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could! They actually found a few bugs in the simulators 😢 But I think we can disable at least until we can run them more rapidly (opcode gas tweaks is a prereq to that)
| await worldStateService.close(); | ||
| }); | ||
|
|
||
| describe.each(groupedSpamConfigs)('$opcode', ({ configs }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think jest runs tests in parallel by default. You might want to add a comment to run this with
HARDWARE_CONCURRENCY=32 yarn test .... -- --runInBand
which i think would run them serially
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think jest runs all tests in 1 file serially. It runs different test files in parallel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll double check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed. Tests within 1 files are all run serially by jest. It parallelizes runs of distinct test files
fcarreiro
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it was not clear from the comments: I think the worst cases are good for simulation, but cannot be trusted for proving due to deduplication.
0185ab0 to
58b0297
Compare
1239e75 to
e6eec10
Compare
e6eec10 to
e6a8f44
Compare
# AVM Opcode Spammer
## Overview
The **Opcode Spammer** is a bytecode-building framework for spamming AVM
opcodes. It generates bytecode that repeatedly executes target opcodes
until the transaction runs out of gas, allowing us to measure worst-case
simulation and proving times, and confirm that all such transactions are
simulatable and provable.
## What's left after this
1. Spammer tests for external (regular/static) calls/returns/reverts,
internalcalls, min/max scaling factors for dynamic gas opcodes, all wire
formats (currently only doing smallest wire formats)
2. Scripts to run proving benchmarks with dedicated resources (and maybe
only a subset of the cases)
4. Use random numbers (preferably seeded) instead of consts like 42
5. Consider using calldatacopy for "unique" values (for EMITNULLIFIER
and SSTORE) (one big cdc instead of 1 add per target instr)
7. Make the tests for the following more meaningful:
- CALLDATACOPY
- RETURNDATACOPY
- RETURNDATASIZE
- GETENVVAR
- GETCONTRACTINSTANCE
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ SPAM_CONFIGS │
│ Record<Opcode, SpamConfig[]> │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ADD_8 │ │ POSEIDON2 │ │EMITNULLIFIER│ ... │
│ │ [7 configs] │ │ [1 config] │ │ [1 config] │ │
│ │ (per type) │ │ │ │ (limit=63) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ getSpamConfigsPerOpcode() │
│ Returns { opcodes, config[] } for test iteration │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ testOpcodeSpamCase() │
│ Routes to appropriate bytecode generator & executes test │
│ │
│ config.limit === undefined? │
│ YES → testStandardOpcodeSpam() │
│ NO → testSideEffectOpcodeSpam() │
└─────────────────────────────────────────────────────────────────┘
```
## Two Execution Strategies
### Strategy 1: Standard Opcodes (Gas-Limited)
For opcodes without per-TX limits (arithmetic, comparisons, memory ops,
etc.), we create a single contract with an infinite loop:
```
┌────────────────────────────────────────────────────────────────┐
│ SINGLE CONTRACT │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ SETUP PHASE │ │
│ │ SET mem[0] = initial_value │ │
│ │ SET mem[1] = operand │ │
│ │ ... │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ LOOP (fills remaining bytecode space) ◄─────┐ │ │
│ │ TARGET_OPCODE ─┐ │ │ │
│ │ TARGET_OPCODE │ unrolled N times │ │ │
│ │ TARGET_OPCODE │ (N = available_bytes / instr_size)│ │ │
│ │ ... ─┘ │ │ │
│ │ JUMP back ──────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Executes until: OUT OF GAS │
└────────────────────────────────────────────────────────────────┘
```
**Bytecode Layout:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 0x00: SET instructions (setup) │
│ ... │
│ 0xNN: ┌─── LOOP START ◄──────────────────────────────────────┐ │
│ │ TARGET_OPCODE │ │
│ │ TARGET_OPCODE (unrolled to fill max bytecode size) │ │
│ │ TARGET_OPCODE │ │
│ │ ... │ │
│ └─► JUMP 0xNN ─────────────────────────────────────────┘ │
│ MAX_BYTECODE_BYTES │
└─────────────────────────────────────────────────────────────────┘
```
### Strategy 2: Side-Effect Limited Opcodes (Nested Call Pattern)
For opcodes with per-TX limits (EMITNOTEHASH, EMITNULLIFIER,
SENDL2TOL1MSG, etc.), we use a two-contract pattern where the inner
contract executes side effects up to the limit, then REVERTs to discard
them:
```
┌─────────────────────────────────────────────────────────────────┐
│ OUTER CONTRACT │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SETUP │ │
│ │ CALLDATACOPY inner_address from calldata[0] │ │
│ │ SET l2Gas = MAX_UINT32 │ │
│ │ SET daGas = MAX_UINT32 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ LOOP ◄────┐ │ │
│ │ CALL inner_contract ──────────────────────┐ │ │ │
│ │ JUMP back ─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ Executes until: OUT OF GAS │ │
└───────────────────────────────────────────────│─────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ INNER CONTRACT │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SETUP │ │
│ │ SET initial values for side-effect opcode │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ BODY (unrolled, NOT a loop) │ │
│ │ SIDE_EFFECT_OPCODE ─┐ │ │
│ │ SIDE_EFFECT_OPCODE │ repeated `limit` times │ │
│ │ SIDE_EFFECT_OPCODE │ (e.g., 64 for EMITNOTEHASH) │ │
│ │ ... ─┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ CLEANUP │ │
│ │ REVERT (discards all side effects from this call) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Why this pattern?**
Side-effect opcodes have per-TX limits:
- `EMITNOTEHASH`: max 64 per TX
- `EMITNULLIFIER`: max 63 per TX (one reserved for TX nullifier)
- `SENDL2TOL1MSG`: max 8 per TX
- `EMITUNENCRYPTEDLOG`: limited by total log payload size
By having the inner contract REVERT after emitting side effects, those
effects are discarded, allowing the outer contract to call it again.
This enables thousands of opcode executions per TX instead of just the
limit.
## SpamConfig Structure
```typescript
interface SpamConfig {
// Memory cells to initialize before the loop
setup: SetupItem[];
// Factory to create target instruction(s) to spam
targetInstructions: () => Bufferable[];
// Instructions to run after target spam (for side-effect opcodes)
cleanupInstructions?: () => Bufferable[];
// Per-TX limit (triggers nested call pattern if set)
limit?: number;
// Label for test display (e.g., "UINT32" for type variants)
label?: string;
}
```
## Type Variants
Many opcodes support multiple types. The spammer tests each type variant
separately:
```typescript
// ADD_8 has 7 configs - one per supported type
[Opcode.ADD_8]: ALL_TAGS.map(tag => ({
label: TypeTag[tag], // "FIELD", "UINT1", "UINT8", etc.
setup: [
{ offset: 0, value: withTag(1n, tag) },
{ offset: 1, value: withTag(1n, tag) },
],
targetInstructions: () => [new Add(0, 0, 1, 0).as(Opcode.ADD_8, Add.wireFormat8)],
})),
```
Test output hierarchy:
```
ADD_8
├── ADD_8/FIELD ✓
├── ADD_8/UINT1 ✓
├── ADD_8/UINT8 ✓
├── ADD_8/UINT16 ✓
├── ADD_8/UINT32 ✓
├── ADD_8/UINT64 ✓
└── ADD_8/UINT128 ✓
```
### In Tests
```typescript
import { getSpamConfigsPerOpcode, testOpcodeSpamCase } from '@aztec/simulator/public/fixtures';
const groupedSpamConfigs = getSpamConfigsPerOpcode();
describe.each(groupedSpamConfigs)('$opcode', ({ configs }) => {
it.each(configs)('$label', async config => {
await testOpcodeSpamCase(tester, config, expectToBeTrue);
});
});
```
## Test Suites
### Simulation Benchmarks
`yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts`
Runs opcode spam through the C++ simulator (and optionally TS vs C++
comparison).
### Proving Benchmarks
`yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.ts`
Runs opcode spam through full AVM proving. Skipped in CI (meant for
local measurement).
# AVM Opcode Spammer
## Overview
The **Opcode Spammer** is a bytecode-building framework for spamming AVM
opcodes. It generates bytecode that repeatedly executes target opcodes
until the transaction runs out of gas, allowing us to measure worst-case
simulation and proving times, and confirm that all such transactions are
simulatable and provable.
## What's left after this
1. Spammer tests for external (regular/static) calls/returns/reverts,
internalcalls, min/max scaling factors for dynamic gas opcodes, all wire
formats (currently only doing smallest wire formats)
2. Scripts to run proving benchmarks with dedicated resources (and maybe
only a subset of the cases)
4. Use random numbers (preferably seeded) instead of consts like 42
5. Consider using calldatacopy for "unique" values (for EMITNULLIFIER
and SSTORE) (one big cdc instead of 1 add per target instr)
7. Make the tests for the following more meaningful:
- CALLDATACOPY
- RETURNDATACOPY
- RETURNDATASIZE
- GETENVVAR
- GETCONTRACTINSTANCE
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ SPAM_CONFIGS │
│ Record<Opcode, SpamConfig[]> │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ADD_8 │ │ POSEIDON2 │ │EMITNULLIFIER│ ... │
│ │ [7 configs] │ │ [1 config] │ │ [1 config] │ │
│ │ (per type) │ │ │ │ (limit=63) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ getSpamConfigsPerOpcode() │
│ Returns { opcodes, config[] } for test iteration │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ testOpcodeSpamCase() │
│ Routes to appropriate bytecode generator & executes test │
│ │
│ config.limit === undefined? │
│ YES → testStandardOpcodeSpam() │
│ NO → testSideEffectOpcodeSpam() │
└─────────────────────────────────────────────────────────────────┘
```
## Two Execution Strategies
### Strategy 1: Standard Opcodes (Gas-Limited)
For opcodes without per-TX limits (arithmetic, comparisons, memory ops,
etc.), we create a single contract with an infinite loop:
```
┌────────────────────────────────────────────────────────────────┐
│ SINGLE CONTRACT │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ SETUP PHASE │ │
│ │ SET mem[0] = initial_value │ │
│ │ SET mem[1] = operand │ │
│ │ ... │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ LOOP (fills remaining bytecode space) ◄─────┐ │ │
│ │ TARGET_OPCODE ─┐ │ │ │
│ │ TARGET_OPCODE │ unrolled N times │ │ │
│ │ TARGET_OPCODE │ (N = available_bytes / instr_size)│ │ │
│ │ ... ─┘ │ │ │
│ │ JUMP back ──────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Executes until: OUT OF GAS │
└────────────────────────────────────────────────────────────────┘
```
**Bytecode Layout:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 0x00: SET instructions (setup) │
│ ... │
│ 0xNN: ┌─── LOOP START ◄──────────────────────────────────────┐ │
│ │ TARGET_OPCODE │ │
│ │ TARGET_OPCODE (unrolled to fill max bytecode size) │ │
│ │ TARGET_OPCODE │ │
│ │ ... │ │
│ └─► JUMP 0xNN ─────────────────────────────────────────┘ │
│ MAX_BYTECODE_BYTES │
└─────────────────────────────────────────────────────────────────┘
```
### Strategy 2: Side-Effect Limited Opcodes (Nested Call Pattern)
For opcodes with per-TX limits (EMITNOTEHASH, EMITNULLIFIER,
SENDL2TOL1MSG, etc.), we use a two-contract pattern where the inner
contract executes side effects up to the limit, then REVERTs to discard
them:
```
┌─────────────────────────────────────────────────────────────────┐
│ OUTER CONTRACT │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SETUP │ │
│ │ CALLDATACOPY inner_address from calldata[0] │ │
│ │ SET l2Gas = MAX_UINT32 │ │
│ │ SET daGas = MAX_UINT32 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ LOOP ◄────┐ │ │
│ │ CALL inner_contract ──────────────────────┐ │ │ │
│ │ JUMP back ─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ Executes until: OUT OF GAS │ │
└───────────────────────────────────────────────│─────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ INNER CONTRACT │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SETUP │ │
│ │ SET initial values for side-effect opcode │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ BODY (unrolled, NOT a loop) │ │
│ │ SIDE_EFFECT_OPCODE ─┐ │ │
│ │ SIDE_EFFECT_OPCODE │ repeated `limit` times │ │
│ │ SIDE_EFFECT_OPCODE │ (e.g., 64 for EMITNOTEHASH) │ │
│ │ ... ─┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ CLEANUP │ │
│ │ REVERT (discards all side effects from this call) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Why this pattern?**
Side-effect opcodes have per-TX limits:
- `EMITNOTEHASH`: max 64 per TX
- `EMITNULLIFIER`: max 63 per TX (one reserved for TX nullifier)
- `SENDL2TOL1MSG`: max 8 per TX
- `EMITUNENCRYPTEDLOG`: limited by total log payload size
By having the inner contract REVERT after emitting side effects, those
effects are discarded, allowing the outer contract to call it again.
This enables thousands of opcode executions per TX instead of just the
limit.
## SpamConfig Structure
```typescript
interface SpamConfig {
// Memory cells to initialize before the loop
setup: SetupItem[];
// Factory to create target instruction(s) to spam
targetInstructions: () => Bufferable[];
// Instructions to run after target spam (for side-effect opcodes)
cleanupInstructions?: () => Bufferable[];
// Per-TX limit (triggers nested call pattern if set)
limit?: number;
// Label for test display (e.g., "UINT32" for type variants)
label?: string;
}
```
## Type Variants
Many opcodes support multiple types. The spammer tests each type variant
separately:
```typescript
// ADD_8 has 7 configs - one per supported type
[Opcode.ADD_8]: ALL_TAGS.map(tag => ({
label: TypeTag[tag], // "FIELD", "UINT1", "UINT8", etc.
setup: [
{ offset: 0, value: withTag(1n, tag) },
{ offset: 1, value: withTag(1n, tag) },
],
targetInstructions: () => [new Add(0, 0, 1, 0).as(Opcode.ADD_8, Add.wireFormat8)],
})),
```
Test output hierarchy:
```
ADD_8
├── ADD_8/FIELD ✓
├── ADD_8/UINT1 ✓
├── ADD_8/UINT8 ✓
├── ADD_8/UINT16 ✓
├── ADD_8/UINT32 ✓
├── ADD_8/UINT64 ✓
└── ADD_8/UINT128 ✓
```
### In Tests
```typescript
import { getSpamConfigsPerOpcode, testOpcodeSpamCase } from '@aztec/simulator/public/fixtures';
const groupedSpamConfigs = getSpamConfigsPerOpcode();
describe.each(groupedSpamConfigs)('$opcode', ({ configs }) => {
it.each(configs)('$label', async config => {
await testOpcodeSpamCase(tester, config, expectToBeTrue);
});
});
```
## Test Suites
### Simulation Benchmarks
`yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts`
Runs opcode spam through the C++ simulator (and optionally TS vs C++
comparison).
### Proving Benchmarks
`yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.ts`
Runs opcode spam through full AVM proving. Skipped in CI (meant for
local measurement).

AVM Opcode Spammer
Overview
The Opcode Spammer is a bytecode-building framework for spamming AVM opcodes. It generates bytecode that repeatedly executes target opcodes until the transaction runs out of gas, allowing us to measure worst-case simulation and proving times, and confirm that all such transactions are simulatable and provable.
What's left after this
Architecture
Two Execution Strategies
Strategy 1: Standard Opcodes (Gas-Limited)
For opcodes without per-TX limits (arithmetic, comparisons, memory ops, etc.), we create a single contract with an infinite loop:
Bytecode Layout:
Strategy 2: Side-Effect Limited Opcodes (Nested Call Pattern)
For opcodes with per-TX limits (EMITNOTEHASH, EMITNULLIFIER, SENDL2TOL1MSG, etc.), we use a two-contract pattern where the inner contract executes side effects up to the limit, then REVERTs to discard them:
Why this pattern?
Side-effect opcodes have per-TX limits:
EMITNOTEHASH: max 64 per TXEMITNULLIFIER: max 63 per TX (one reserved for TX nullifier)SENDL2TOL1MSG: max 8 per TXEMITUNENCRYPTEDLOG: limited by total log payload sizeBy having the inner contract REVERT after emitting side effects, those effects are discarded, allowing the outer contract to call it again. This enables thousands of opcode executions per TX instead of just the limit.
SpamConfig Structure
Type Variants
Many opcodes support multiple types. The spammer tests each type variant separately:
Test output hierarchy:
In Tests
Test Suites
Simulation Benchmarks
yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.tsRuns opcode spam through the C++ simulator (and optionally TS vs C++ comparison).
Proving Benchmarks
yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.tsRuns opcode spam through full AVM proving. Skipped in CI (meant for local measurement).