@aztec/simulator 3.0.0-nightly.20251216 → 3.0.0-nightly.20251218

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dest/public/debug_fn_name.d.ts +1 -1
  2. package/dest/public/debug_fn_name.d.ts.map +1 -1
  3. package/dest/public/debug_fn_name.js +10 -3
  4. package/dest/public/fixtures/custom_bytecode_tester.d.ts +28 -6
  5. package/dest/public/fixtures/custom_bytecode_tester.d.ts.map +1 -1
  6. package/dest/public/fixtures/custom_bytecode_tester.js +36 -12
  7. package/dest/public/fixtures/custom_bytecode_tests.d.ts +3 -1
  8. package/dest/public/fixtures/custom_bytecode_tests.d.ts.map +1 -1
  9. package/dest/public/fixtures/custom_bytecode_tests.js +54 -10
  10. package/dest/public/fixtures/index.d.ts +3 -1
  11. package/dest/public/fixtures/index.d.ts.map +1 -1
  12. package/dest/public/fixtures/index.js +2 -0
  13. package/dest/public/fixtures/minimal_public_tx.js +2 -2
  14. package/dest/public/fixtures/opcode_spammer.d.ts +86 -0
  15. package/dest/public/fixtures/opcode_spammer.d.ts.map +1 -0
  16. package/dest/public/fixtures/opcode_spammer.js +1539 -0
  17. package/dest/public/fixtures/public_tx_simulation_tester.d.ts +2 -2
  18. package/dest/public/fixtures/public_tx_simulation_tester.d.ts.map +1 -1
  19. package/dest/public/fixtures/public_tx_simulation_tester.js +19 -7
  20. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts +1 -1
  21. package/dest/public/public_tx_simulator/contract_provider_for_cpp.d.ts.map +1 -1
  22. package/dest/public/public_tx_simulator/contract_provider_for_cpp.js +15 -11
  23. package/dest/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.d.ts +1 -1
  24. package/dest/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.d.ts.map +1 -1
  25. package/dest/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.js +2 -1
  26. package/package.json +16 -16
  27. package/src/public/debug_fn_name.ts +10 -3
  28. package/src/public/fixtures/custom_bytecode_tester.ts +53 -19
  29. package/src/public/fixtures/custom_bytecode_tests.ts +70 -10
  30. package/src/public/fixtures/index.ts +6 -0
  31. package/src/public/fixtures/minimal_public_tx.ts +2 -2
  32. package/src/public/fixtures/opcode_spammer.ts +1516 -0
  33. package/src/public/fixtures/public_tx_simulation_tester.ts +19 -5
  34. package/src/public/public_tx_simulator/contract_provider_for_cpp.ts +16 -11
  35. package/src/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.ts +2 -1
@@ -0,0 +1,1539 @@
1
+ /**
2
+ * Opcode Spammer - A minimal, data-driven opcode spammer for AVM gas benchmarking.
3
+ *
4
+ * Design principles:
5
+ * 1. Data over code: Opcode behavior is configuration, not control flow
6
+ * 2. Derive, don't declare: Categories and strategies follow from the data
7
+ * 3. Maximize coverage: Fill bytecode to the limit for accurate gas measurement
8
+ * 4. Smallest wire format: Use _8 variants over _16 to fit more instructions per loop
9
+ * 5. Single file: Everything in one module
10
+ *
11
+ * ## Architecture
12
+ *
13
+ * ```
14
+ * ┌─────────────────────────────────────────────────────────────────┐
15
+ * │ SPAM_CONFIGS │
16
+ * │ Record<Opcode, SpamConfig[]> │
17
+ * │ │
18
+ * │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
19
+ * │ │ ADD_8 │ │ POSEIDON2 │ │EMITNULLIFIER│ ... │
20
+ * │ │ [7 configs] │ │ [1 config] │ │ [1 config] │ │
21
+ * │ │ (per type) │ │ │ │ (limit=63) │ │
22
+ * │ └─────────────┘ └─────────────┘ └─────────────┘ │
23
+ * └─────────────────────────────────────────────────────────────────┘
24
+ * │
25
+ * ▼
26
+ * ┌─────────────────────────────────────────────────────────────────┐
27
+ * │ getSpamConfigsPerOpcode() │
28
+ * │ Returns { opcodes, config[] } for test iteration │
29
+ * └─────────────────────────────────────────────────────────────────┘
30
+ * │
31
+ * ▼
32
+ * ┌─────────────────────────────────────────────────────────────────┐
33
+ * │ testOpcodeSpamCase() │
34
+ * │ Routes to appropriate bytecode generator & executes test │
35
+ * │ │
36
+ * │ config.limit === undefined? │
37
+ * │ YES → testStandardOpcodeSpam() │
38
+ * │ NO → testSideEffectOpcodeSpam() │
39
+ * └─────────────────────────────────────────────────────────────────┘
40
+ * ```
41
+ *
42
+ * ## Two Execution Strategies
43
+ *
44
+ * ### Strategy 1: Standard Opcodes (Gas-Limited)
45
+ *
46
+ * For opcodes without per-TX limits (arithmetic, comparisons, memory ops, etc.), we create a single contract with an infinite loop:
47
+ *
48
+ * ```
49
+ * ┌────────────────────────────────────────────────────────────────┐
50
+ * │ SINGLE CONTRACT │
51
+ * │ │
52
+ * │ ┌──────────────────────────────────────────────────────────┐ │
53
+ * │ │ SETUP PHASE │ │
54
+ * │ │ SET mem[0] = initial_value │ │
55
+ * │ │ SET mem[1] = operand │ │
56
+ * │ │ ... │ │
57
+ * │ └──────────────────────────────────────────────────────────┘ │
58
+ * │ │ │
59
+ * │ ▼ │
60
+ * │ ┌──────────────────────────────────────────────────────────┐ │
61
+ * │ │ LOOP (fills remaining bytecode space) ◄─────┐ │ │
62
+ * │ │ TARGET_OPCODE ─┐ │ │ │
63
+ * │ │ TARGET_OPCODE │ unrolled N times │ │ │
64
+ * │ │ TARGET_OPCODE │ (N = available_bytes / instr_size)│ │ │
65
+ * │ │ ... ─┘ │ │ │
66
+ * │ │ JUMP back ──────────────────────────────────────────┘ │ │
67
+ * │ └──────────────────────────────────────────────────────────┘ │
68
+ * │ │
69
+ * │ Executes until: OUT OF GAS │
70
+ * └────────────────────────────────────────────────────────────────┘
71
+ * ```
72
+ *
73
+ * **Bytecode Layout:**
74
+ * ```
75
+ * ┌─────────────────────────────────────────────────────────────────┐
76
+ * │ 0x00: SET instructions (setup) │
77
+ * │ ... │
78
+ * │ 0xNN: ┌─── LOOP START ◄──────────────────────────────────────┐ │
79
+ * │ │ TARGET_OPCODE │ │
80
+ * │ │ TARGET_OPCODE (unrolled to fill max bytecode size) │ │
81
+ * │ │ TARGET_OPCODE │ │
82
+ * │ │ ... │ │
83
+ * │ └─► JUMP 0xNN ─────────────────────────────────────────┘ │
84
+ * │ MAX_BYTECODE_BYTES │
85
+ * └─────────────────────────────────────────────────────────────────┘
86
+ * ```
87
+ *
88
+ * ### Strategy 2: Side-Effect Limited Opcodes (Nested Call Pattern)
89
+ *
90
+ * 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:
91
+ *
92
+ * ```
93
+ * ┌─────────────────────────────────────────────────────────────────┐
94
+ * │ OUTER CONTRACT │
95
+ * │ │
96
+ * │ ┌───────────────────────────────────────────────────────────┐ │
97
+ * │ │ SETUP │ │
98
+ * │ │ CALLDATACOPY inner_address from calldata[0] │ │
99
+ * │ │ SET l2Gas = MAX_UINT32 │ │
100
+ * │ │ SET daGas = MAX_UINT32 │ │
101
+ * │ └───────────────────────────────────────────────────────────┘ │
102
+ * │ │ │
103
+ * │ ▼ │
104
+ * │ ┌───────────────────────────────────────────────────────────┐ │
105
+ * │ │ LOOP ◄────┐ │ │
106
+ * │ │ CALL inner_contract ──────────────────────┐ │ │ │
107
+ * │ │ JUMP back ─────────────────────────────────────────────┘ │ │
108
+ * │ └───────────────────────────────────────────────────────────┘ │
109
+ * │ │ │
110
+ * │ Executes until: OUT OF GAS │ │
111
+ * └───────────────────────────────────────────────│─────────────────┘
112
+ * │
113
+ * ▼
114
+ * ┌─────────────────────────────────────────────────────────────────┐
115
+ * │ INNER CONTRACT │
116
+ * │ │
117
+ * │ ┌───────────────────────────────────────────────────────────┐ │
118
+ * │ │ SETUP │ │
119
+ * │ │ SET initial values for side-effect opcode │ │
120
+ * │ └───────────────────────────────────────────────────────────┘ │
121
+ * │ │ │
122
+ * │ ▼ │
123
+ * │ ┌───────────────────────────────────────────────────────────┐ │
124
+ * │ │ BODY (unrolled, NOT a loop) │ │
125
+ * │ │ SIDE_EFFECT_OPCODE ─┐ │ │
126
+ * │ │ SIDE_EFFECT_OPCODE │ repeated `limit` times │ │
127
+ * │ │ SIDE_EFFECT_OPCODE │ (e.g., 64 for EMITNOTEHASH) │ │
128
+ * │ │ ... ─┘ │ │
129
+ * │ └───────────────────────────────────────────────────────────┘ │
130
+ * │ │ │
131
+ * │ ▼ │
132
+ * │ ┌───────────────────────────────────────────────────────────┐ │
133
+ * │ │ CLEANUP │ │
134
+ * │ │ REVERT (discards all side effects from this call) │ │
135
+ * │ └───────────────────────────────────────────────────────────┘ │
136
+ * │ │
137
+ * └─────────────────────────────────────────────────────────────────┘
138
+ * ```
139
+ *
140
+ * **Why this pattern?**
141
+ *
142
+ * Side-effect opcodes have per-TX limits:
143
+ * - `EMITNOTEHASH`: max 64 per TX
144
+ * - `EMITNULLIFIER`: max 63 per TX (one reserved for TX nullifier)
145
+ * - `SENDL2TOL1MSG`: max 8 per TX
146
+ * - `EMITUNENCRYPTEDLOG`: limited by total log payload size
147
+ *
148
+ * 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.
149
+ *
150
+ */ import { FLAT_PUBLIC_LOGS_PAYLOAD_LENGTH, MAX_L2_TO_L1_MSGS_PER_TX, MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, MAX_PUBLIC_LOG_SIZE_IN_FIELDS, PUBLIC_LOG_HEADER_LENGTH } from '@aztec/constants';
151
+ import { Grumpkin } from '@aztec/foundation/crypto/grumpkin';
152
+ import { randomBigInt } from '@aztec/foundation/crypto/random';
153
+ import { Fr } from '@aztec/foundation/curves/bn254';
154
+ import assert from 'assert';
155
+ import { Field, TaggedMemory, TypeTag, Uint1, Uint32 } from '../avm/avm_memory_types.js';
156
+ import { Add, And, Call, CalldataCopy, Cast, DebugLog, Div, EcAdd, EmitNoteHash, EmitNullifier, EmitUnencryptedLog, Eq, FieldDiv, GetContractInstance, GetEnvVar, InternalCall, InternalReturn, Jump, JumpI, KeccakF1600, L1ToL2MessageExists, Lt, Lte, Mov, Mul, Not, NoteHashExists, NullifierExists, Or, Poseidon2, Return, ReturndataCopy, ReturndataSize, Revert, SLoad, SStore, SendL2ToL1Message, Set, Sha256Compression, Shl, Shr, StaticCall, Sub, SuccessCopy, ToRadixBE, Xor } from '../avm/opcodes/index.js';
157
+ import { encodeToBytecode } from '../avm/serialization/bytecode_serialization.js';
158
+ import { Opcode } from '../avm/serialization/instruction_serialization.js';
159
+ import { deployCustomBytecode, executeCustomBytecode } from './custom_bytecode_tester.js';
160
+ // ============================================================================
161
+ // Constants
162
+ // ============================================================================
163
+ /**
164
+ * Maximum bytecode size in bytes.
165
+ *
166
+ * Bytecode is encoded as fields using bufferAsFields():
167
+ * - 1 field for the byte length
168
+ * - ceil(byteLength / 31) fields for the data (31 bytes per field)
169
+ *
170
+ * So: 1 + ceil(byteLength / 31) <= MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS
171
+ * ceil(byteLength / 31) <= MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1
172
+ * byteLength <= (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * 31
173
+ */ const BYTES_PER_FIELD = Fr.SIZE_IN_BYTES - 1; // 31 bytes of data per field
174
+ const MAX_BYTECODE_BYTES = (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * BYTES_PER_FIELD;
175
+ const JUMP_SIZE = encodeToBytecode([
176
+ new Jump(0)
177
+ ]).length; // JUMP_32
178
+ const INTERNALCALL_SIZE = encodeToBytecode([
179
+ new InternalCall(0)
180
+ ]).length;
181
+ // ============================================================================
182
+ // Type Variant Helpers (for generating multiple configs per opcode)
183
+ // ============================================================================
184
+ // Not using these sets directly because we want to control order
185
+ //const ALL_TAGS = Array.from(VALID_TAGS);
186
+ //const INT_TAGS = Array.from(INTEGRAL_TAGS);
187
+ // Ordered so that limiting #configs per opcode still tests max size (Field)
188
+ const ALL_TAGS = [
189
+ TypeTag.FIELD,
190
+ TypeTag.UINT1,
191
+ TypeTag.UINT8,
192
+ TypeTag.UINT16,
193
+ TypeTag.UINT32,
194
+ TypeTag.UINT64,
195
+ TypeTag.UINT128
196
+ ];
197
+ // ordered so that limiting #configs per opcode still tests max size
198
+ const INT_TAGS = [
199
+ TypeTag.UINT128,
200
+ TypeTag.UINT1,
201
+ TypeTag.UINT8,
202
+ TypeTag.UINT16,
203
+ TypeTag.UINT32,
204
+ TypeTag.UINT64
205
+ ];
206
+ /** Build from tag truncating - shorter name */ function withTag(v, tag) {
207
+ return TaggedMemory.buildFromTagTruncating(v, tag);
208
+ }
209
+ // ============================================================================
210
+ // Random Value Helpers (seeded via SEED env var for reproducibility)
211
+ // ============================================================================
212
+ /** Modulus (really just max+1) for each integer type tag */ const TAG_MODULI = {
213
+ [TypeTag.UINT1]: 2n,
214
+ [TypeTag.UINT8]: 256n,
215
+ [TypeTag.UINT16]: 65536n,
216
+ [TypeTag.UINT32]: 0x1_0000_0000n,
217
+ [TypeTag.UINT64]: 0x1_0000_0000_0000_0000n,
218
+ [TypeTag.UINT128]: 0x1_0000_0000_0000_0000_0000_0000_0000_0000n,
219
+ [TypeTag.FIELD]: Fr.MODULUS
220
+ };
221
+ /** Generate a random value with the given type tag. Uses SEED env var if set. */ function randomWithTag(tag) {
222
+ const modulus = TAG_MODULI[tag];
223
+ if (modulus === undefined) {
224
+ throw new Error(`Unsupported tag for random generation: ${TypeTag[tag]}`);
225
+ }
226
+ const value = randomBigInt(modulus);
227
+ return TaggedMemory.buildFromTagTruncating(value, tag);
228
+ }
229
+ /** Generate a random non-zero value with the given type tag (for division). */ function randomNonZeroWithTag(tag) {
230
+ const modulus = TAG_MODULI[tag];
231
+ if (modulus === undefined) {
232
+ throw new Error(`Unsupported tag for random generation: ${TypeTag[tag]}`);
233
+ }
234
+ // Generate random in range [1, max) by generating [0, max-1) and adding 1
235
+ const value = randomBigInt(modulus - 1n) + 1n;
236
+ return TaggedMemory.buildFromTagTruncating(value, tag);
237
+ }
238
+ /** Generate a random non-zero Field value (for field division). */ function randomNonZeroField() {
239
+ return new Field(randomBigInt(Fr.MODULUS - 1n) + 1n);
240
+ }
241
+ /** Reserved memory offsets for external call loop (used by CALL spam and side-effect opcodes) */ const CONST_0_OFFSET = 0; // Uint32(0)
242
+ const CONST_1_OFFSET = 1; // Uint32(1)
243
+ const CONST_MAX_U32_OFFSET = 2; // Uint32(MAX_U32)
244
+ const CALL_ADDR_OFFSET = 3; // copy addr from calldata to here, and then use this addr for CALL
245
+ const CALL_ARGS_OFFSET = CALL_ADDR_OFFSET; // address is the arg to send to CALL
246
+ const CALL_COPY_SIZE_OFFSET = CONST_1_OFFSET; // copy size = 1 (forward calldata[0])
247
+ const CALL_CALLDATA_INDEX_OFFSET = CONST_0_OFFSET; // calldata[0]
248
+ const CALL_L2_GAS_OFFSET = CONST_MAX_U32_OFFSET; // MAX_U32 gets capped to remaining gas by AVM
249
+ const CALL_DA_GAS_OFFSET = CONST_MAX_U32_OFFSET; // MAX_U32 gets capped to remaining gas by AVM
250
+ const CALL_ARGS_SIZE_OFFSET = CONST_1_OFFSET; // argsSize = 1 (forward calldata[0] - might contain contract address)
251
+ const MAX_U32 = 0xffffffffn;
252
+ /**
253
+ * A SpamConfig for to make external CALLs to an address specified in calldata[0].
254
+ */ const EXTERNAL_CALL_CONFIG = {
255
+ setup: [
256
+ // calldata will contain 1 item: the external call address
257
+ {
258
+ offset: CONST_0_OFFSET,
259
+ value: new Uint32(0)
260
+ },
261
+ {
262
+ offset: CONST_1_OFFSET,
263
+ value: new Uint32(1)
264
+ },
265
+ {
266
+ offset: CONST_MAX_U32_OFFSET,
267
+ value: new Uint32(MAX_U32)
268
+ },
269
+ ()=>[
270
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ CALL_COPY_SIZE_OFFSET, /*cdStartOffset=*/ CALL_CALLDATA_INDEX_OFFSET, /*dstOffset=*/ CALL_ADDR_OFFSET)
271
+ ]
272
+ ],
273
+ targetInstructions: ()=>[
274
+ new Call(/*indirect=*/ 0, /*l2GasOffset=*/ CALL_L2_GAS_OFFSET, /*daGasOffset=*/ CALL_DA_GAS_OFFSET, /*addrOffset=*/ CALL_ADDR_OFFSET, /*argsSizeOffset=*/ CALL_ARGS_SIZE_OFFSET, /*argsOffset=*/ CALL_ARGS_OFFSET)
275
+ ],
276
+ addressAsCalldata: true
277
+ };
278
+ const STATIC_CALL_CONFIG = {
279
+ setup: [
280
+ // calldata will contain 1 item: the external call address
281
+ {
282
+ offset: CONST_0_OFFSET,
283
+ value: new Uint32(0)
284
+ },
285
+ {
286
+ offset: CONST_1_OFFSET,
287
+ value: new Uint32(1)
288
+ },
289
+ {
290
+ offset: CONST_MAX_U32_OFFSET,
291
+ value: new Uint32(MAX_U32)
292
+ },
293
+ ()=>[
294
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ CALL_COPY_SIZE_OFFSET, /*cdStartOffset=*/ CALL_CALLDATA_INDEX_OFFSET, /*dstOffset=*/ CALL_ADDR_OFFSET)
295
+ ]
296
+ ],
297
+ targetInstructions: ()=>[
298
+ new StaticCall(/*indirect=*/ 0, /*l2GasOffset=*/ CALL_L2_GAS_OFFSET, /*daGasOffset=*/ CALL_DA_GAS_OFFSET, /*addrOffset=*/ CALL_ADDR_OFFSET, /*argsSizeOffset=*/ CALL_ARGS_SIZE_OFFSET, /*argsOffset=*/ CALL_ARGS_OFFSET)
299
+ ],
300
+ addressAsCalldata: true
301
+ };
302
+ // ============================================================================
303
+ // Configuration Map
304
+ // ============================================================================
305
+ /**
306
+ * Opcode spammer configs for ~all opcodes.
307
+ * Each opcode maps to an array of configs (usually one, but can be multiple for type variants, etc.)
308
+ * Uses smallest wire format (_8) for maximum instruction density.
309
+ */ export const SPAM_CONFIGS = {
310
+ // ═══════════════════════════════════════════════════════════════════════════
311
+ // ARITHMETIC - Test with all type variants (random values, seeded via SEED env var)
312
+ // ═══════════════════════════════════════════════════════════════════════════
313
+ [Opcode.ADD_8]: ALL_TAGS.map((tag)=>({
314
+ label: TypeTag[tag],
315
+ setup: [
316
+ {
317
+ offset: 0,
318
+ value: randomWithTag(tag)
319
+ },
320
+ {
321
+ offset: 1,
322
+ value: randomWithTag(tag)
323
+ }
324
+ ],
325
+ targetInstructions: ()=>[
326
+ new Add(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.ADD_8, Add.wireFormat8)
327
+ ]
328
+ })),
329
+ [Opcode.SUB_8]: ALL_TAGS.map((tag)=>({
330
+ label: TypeTag[tag],
331
+ setup: [
332
+ {
333
+ offset: 0,
334
+ value: randomWithTag(tag)
335
+ },
336
+ {
337
+ offset: 1,
338
+ value: randomWithTag(tag)
339
+ }
340
+ ],
341
+ targetInstructions: ()=>[
342
+ new Sub(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.SUB_8, Sub.wireFormat8)
343
+ ]
344
+ })),
345
+ [Opcode.MUL_8]: ALL_TAGS.map((tag)=>({
346
+ label: TypeTag[tag],
347
+ setup: [
348
+ {
349
+ offset: 0,
350
+ value: randomWithTag(tag)
351
+ },
352
+ {
353
+ offset: 1,
354
+ value: randomWithTag(tag)
355
+ }
356
+ ],
357
+ targetInstructions: ()=>[
358
+ new Mul(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.MUL_8, Mul.wireFormat8)
359
+ ]
360
+ })),
361
+ // DIV doesn't support FIELD type
362
+ [Opcode.DIV_8]: INT_TAGS.map((tag)=>({
363
+ label: TypeTag[tag],
364
+ setup: [
365
+ {
366
+ offset: 0,
367
+ value: randomWithTag(tag)
368
+ },
369
+ {
370
+ offset: 1,
371
+ value: randomNonZeroWithTag(tag)
372
+ }
373
+ ],
374
+ targetInstructions: ()=>[
375
+ new Div(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.DIV_8, Div.wireFormat8)
376
+ ]
377
+ })),
378
+ // Field-only
379
+ [Opcode.FDIV_8]: [
380
+ {
381
+ setup: [
382
+ {
383
+ offset: 0,
384
+ value: new Field(Fr.random())
385
+ },
386
+ {
387
+ offset: 1,
388
+ value: randomNonZeroField()
389
+ }
390
+ ],
391
+ targetInstructions: ()=>[
392
+ new FieldDiv(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.FDIV_8, FieldDiv.wireFormat8)
393
+ ]
394
+ }
395
+ ],
396
+ // ═══════════════════════════════════════════════════════════════════════════
397
+ // COMPARATORS - Test with all type variants (random values)
398
+ // ═══════════════════════════════════════════════════════════════════════════
399
+ [Opcode.EQ_8]: ALL_TAGS.map((tag)=>({
400
+ label: TypeTag[tag],
401
+ setup: [
402
+ {
403
+ offset: 0,
404
+ value: randomWithTag(tag)
405
+ },
406
+ {
407
+ offset: 1,
408
+ value: randomWithTag(tag)
409
+ }
410
+ ],
411
+ targetInstructions: ()=>[
412
+ new Eq(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2).as(Opcode.EQ_8, Eq.wireFormat8)
413
+ ]
414
+ })),
415
+ [Opcode.LT_8]: ALL_TAGS.map((tag)=>({
416
+ label: TypeTag[tag],
417
+ setup: [
418
+ {
419
+ offset: 0,
420
+ value: randomWithTag(tag)
421
+ },
422
+ {
423
+ offset: 1,
424
+ value: randomWithTag(tag)
425
+ }
426
+ ],
427
+ targetInstructions: ()=>[
428
+ new Lt(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2).as(Opcode.LT_8, Lt.wireFormat8)
429
+ ]
430
+ })),
431
+ [Opcode.LTE_8]: ALL_TAGS.map((tag)=>({
432
+ label: TypeTag[tag],
433
+ setup: [
434
+ {
435
+ offset: 0,
436
+ value: randomWithTag(tag)
437
+ },
438
+ {
439
+ offset: 1,
440
+ value: randomWithTag(tag)
441
+ }
442
+ ],
443
+ targetInstructions: ()=>[
444
+ new Lte(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2).as(Opcode.LTE_8, Lte.wireFormat8)
445
+ ]
446
+ })),
447
+ // ═══════════════════════════════════════════════════════════════════════════
448
+ // BITWISE - Integer types only (no FIELD) (random values)
449
+ // ═══════════════════════════════════════════════════════════════════════════
450
+ [Opcode.AND_8]: INT_TAGS.map((tag)=>({
451
+ label: TypeTag[tag],
452
+ setup: [
453
+ {
454
+ offset: 0,
455
+ value: randomWithTag(tag)
456
+ },
457
+ {
458
+ offset: 1,
459
+ value: randomWithTag(tag)
460
+ }
461
+ ],
462
+ targetInstructions: ()=>[
463
+ new And(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.AND_8, And.wireFormat8)
464
+ ]
465
+ })),
466
+ [Opcode.OR_8]: INT_TAGS.map((tag)=>({
467
+ label: TypeTag[tag],
468
+ setup: [
469
+ {
470
+ offset: 0,
471
+ value: randomWithTag(tag)
472
+ },
473
+ {
474
+ offset: 1,
475
+ value: randomWithTag(tag)
476
+ }
477
+ ],
478
+ targetInstructions: ()=>[
479
+ new Or(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.OR_8, Or.wireFormat8)
480
+ ]
481
+ })),
482
+ [Opcode.XOR_8]: INT_TAGS.map((tag)=>({
483
+ label: TypeTag[tag],
484
+ setup: [
485
+ {
486
+ offset: 0,
487
+ value: randomWithTag(tag)
488
+ },
489
+ {
490
+ offset: 1,
491
+ value: randomWithTag(tag)
492
+ }
493
+ ],
494
+ targetInstructions: ()=>[
495
+ new Xor(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.XOR_8, Xor.wireFormat8)
496
+ ]
497
+ })),
498
+ [Opcode.NOT_8]: INT_TAGS.map((tag)=>({
499
+ label: TypeTag[tag],
500
+ setup: [
501
+ {
502
+ offset: 0,
503
+ value: randomWithTag(tag)
504
+ }
505
+ ],
506
+ targetInstructions: ()=>[
507
+ new Not(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 0).as(Opcode.NOT_8, Not.wireFormat8)
508
+ ]
509
+ })),
510
+ [Opcode.SHL_8]: INT_TAGS.map((tag)=>({
511
+ label: TypeTag[tag],
512
+ setup: [
513
+ {
514
+ offset: 0,
515
+ value: randomWithTag(tag)
516
+ },
517
+ {
518
+ offset: 1,
519
+ value: withTag(1n, tag)
520
+ }
521
+ ],
522
+ targetInstructions: ()=>[
523
+ new Shl(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.SHL_8, Shl.wireFormat8)
524
+ ]
525
+ })),
526
+ [Opcode.SHR_8]: INT_TAGS.map((tag)=>({
527
+ label: TypeTag[tag],
528
+ setup: [
529
+ {
530
+ offset: 0,
531
+ value: randomWithTag(tag)
532
+ },
533
+ {
534
+ offset: 1,
535
+ value: withTag(1n, tag)
536
+ }
537
+ ],
538
+ targetInstructions: ()=>[
539
+ new Shr(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.SHR_8, Shr.wireFormat8)
540
+ ]
541
+ })),
542
+ // ═══════════════════════════════════════════════════════════════════════════
543
+ // CAST / MOV - Test with all type variants (random values)
544
+ // ═══════════════════════════════════════════════════════════════════════════
545
+ [Opcode.CAST_8]: ALL_TAGS.map((tag)=>({
546
+ label: TypeTag[tag],
547
+ setup: [
548
+ {
549
+ offset: 0,
550
+ value: randomWithTag(tag)
551
+ }
552
+ ],
553
+ targetInstructions: ()=>[
554
+ new Cast(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 1, /*dstTag=*/ TypeTag.UINT32).as(Opcode.CAST_8, Cast.wireFormat8)
555
+ ]
556
+ })),
557
+ [Opcode.MOV_8]: ALL_TAGS.map((tag)=>({
558
+ label: TypeTag[tag],
559
+ setup: [
560
+ {
561
+ offset: 0,
562
+ value: randomWithTag(tag)
563
+ }
564
+ ],
565
+ targetInstructions: ()=>[
566
+ new Mov(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 1).as(Opcode.MOV_8, Mov.wireFormat8)
567
+ ]
568
+ })),
569
+ // ═══════════════════════════════════════════════════════════════════════════
570
+ // MEMORY - SET
571
+ // ═══════════════════════════════════════════════════════════════════════════
572
+ // Not testing all wire formats as they should be roughly the same in terms of simulation
573
+ // and proving time
574
+ //[Opcode.SET_8]: [
575
+ // {
576
+ // setup: [],
577
+ // targetInstructions: () => [new Set(0, 0, TypeTag.UINT8, 42).as(Opcode.SET_8, Set.wireFormat8)],
578
+ // },
579
+ //],
580
+ //[Opcode.SET_16]: [
581
+ // {
582
+ // setup: [],
583
+ // targetInstructions: () => [new Set(0, 0, TypeTag.UINT16, 4242).as(Opcode.SET_16, Set.wireFormat16)],
584
+ // },
585
+ //],
586
+ //[Opcode.SET_32]: [
587
+ // {
588
+ // setup: [],
589
+ // targetInstructions: () => [new Set(0, 0, TypeTag.UINT32, 424242).as(Opcode.SET_32, Set.wireFormat32)],
590
+ // },
591
+ //],
592
+ //[Opcode.SET_64]: [
593
+ // {
594
+ // setup: [],
595
+ // targetInstructions: () => [new Set(0, 0, TypeTag.UINT64, 42424242n).as(Opcode.SET_64, Set.wireFormat64)],
596
+ // },
597
+ //],
598
+ [Opcode.SET_128]: [
599
+ {
600
+ setup: [],
601
+ targetInstructions: ()=>[
602
+ new Set(/*indirect=*/ 0, /*dstOffset=*/ 0, /*inTag=*/ TypeTag.UINT128, /*value=*/ 4242424242424242n).as(Opcode.SET_128, Set.wireFormat128)
603
+ ]
604
+ }
605
+ ],
606
+ //[Opcode.SET_FF]: [
607
+ // {
608
+ // setup: [],
609
+ // targetInstructions: () => [new Set(0, 0, TypeTag.FIELD, 42n).as(Opcode.SET_FF, Set.wireFormatFF)],
610
+ // },
611
+ //],
612
+ // ═══════════════════════════════════════════════════════════════════════════
613
+ // CONTROL FLOW
614
+ // ═══════════════════════════════════════════════════════════════════════════
615
+ [Opcode.JUMP_32]: [
616
+ {
617
+ setup: [],
618
+ // Target will be overwritten by loop builder
619
+ targetInstructions: ()=>[
620
+ new Jump(/*jumpOffset=*/ 0)
621
+ ]
622
+ }
623
+ ],
624
+ [Opcode.JUMPI_32]: [
625
+ {
626
+ setup: [
627
+ {
628
+ offset: 0,
629
+ value: new Uint1(0n)
630
+ }
631
+ ],
632
+ targetInstructions: ()=>[
633
+ new JumpI(/*indirect=*/ 0, /*condOffset=*/ 0, /*loc=*/ 0)
634
+ ]
635
+ }
636
+ ],
637
+ // INTERNALCALL: calls itself infinitely by jumping to its own PC (PC 0, since no setup)
638
+ // Creates infinite recursion until OOG (internal call stack grows forever)
639
+ [Opcode.INTERNALCALL]: [
640
+ {
641
+ setup: [],
642
+ targetInstructions: ()=>[
643
+ new InternalCall(/*loc=*/ 0)
644
+ ]
645
+ }
646
+ ],
647
+ // INTERNALRETURN: needs INTERNALCALL to return without error
648
+ // Layout: INTERNALCALL(10) -> JUMP(0) -> INTERNALRETURN
649
+ // INTERNALCALL jumps to INTERNALRETURN, which returns to JUMP, which loops back
650
+ [Opcode.INTERNALRETURN]: [
651
+ {
652
+ setup: [],
653
+ targetInstructions: ()=>[
654
+ new InternalCall(/*loc=*/ INTERNALCALL_SIZE + JUMP_SIZE),
655
+ new Jump(/*jumpOffset=*/ 0),
656
+ new InternalReturn()
657
+ ]
658
+ }
659
+ ],
660
+ // CALL (EXTERNALCALL): calls the current contract address (self) in a loop
661
+ // Contract address is passed via calldata[0] and propagated to nested calls
662
+ [Opcode.CALL]: [
663
+ EXTERNAL_CALL_CONFIG
664
+ ],
665
+ [Opcode.STATICCALL]: [
666
+ STATIC_CALL_CONFIG
667
+ ],
668
+ // RETURN: terminates execution, so we need to use the two-contract pattern
669
+ // Outer contract CALLs inner contract in a loop, inner contract does RETURN
670
+ [Opcode.RETURN]: [
671
+ {
672
+ setup: [
673
+ {
674
+ offset: 0,
675
+ value: new Uint32(0)
676
+ }
677
+ ],
678
+ targetInstructions: ()=>[
679
+ new Return(/*indirect=*/ 0, /*returnSizeOffset=*/ 0, /*returnOffset=*/ 0)
680
+ ],
681
+ // Use the side-effect-limit pattern (even though it's not a side-effect) as it fits
682
+ // this case (we want to CALL, RETURN, then CALL again back in parent). We omit "cleanup"
683
+ // because we don't need to REVERT as we do for real side-effects.
684
+ limit: 1
685
+ }
686
+ ],
687
+ // REVERT: terminates execution, so we need to use the two-contract pattern
688
+ // Outer contract CALLs inner contract in a loop, inner contract does REVERT
689
+ [Opcode.REVERT_8]: [
690
+ {
691
+ setup: [
692
+ {
693
+ offset: 0,
694
+ value: new Uint32(0)
695
+ }
696
+ ],
697
+ targetInstructions: ()=>[
698
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 0, /*returnOffset=*/ 1).as(Opcode.REVERT_8, Revert.wireFormat8)
699
+ ],
700
+ limit: 1
701
+ }
702
+ ],
703
+ // ═══════════════════════════════════════════════════════════════════════════
704
+ // ENVIRONMENT
705
+ // ═══════════════════════════════════════════════════════════════════════════
706
+ [Opcode.GETENVVAR_16]: [
707
+ {
708
+ setup: [],
709
+ targetInstructions: ()=>[
710
+ new GetEnvVar(/*indirect=*/ 0, /*dstOffset=*/ 0, /*varEnum=*/ 0).as(Opcode.GETENVVAR_16, GetEnvVar.wireFormat16)
711
+ ]
712
+ }
713
+ ],
714
+ // CALLDATACOPY has dynamic gas scaling with copySize
715
+ [Opcode.CALLDATACOPY]: [
716
+ {
717
+ label: 'Min copy size',
718
+ // CalldataCopy with copySize=0 is a no-op but still executes the opcode
719
+ setup: [
720
+ {
721
+ offset: 0,
722
+ value: new Uint32(0n)
723
+ },
724
+ {
725
+ offset: 1,
726
+ value: new Uint32(0n)
727
+ }
728
+ ],
729
+ targetInstructions: ()=>[
730
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*cdStartOffset=*/ 1, /*dstOffset=*/ 2)
731
+ ]
732
+ },
733
+ {
734
+ label: 'Large copy size',
735
+ // Large copySize with large dynamic gas - will OOG quickly
736
+ // NOTE: we don't want it so large that it exceeds memory bounds (MAX_MEMORY_SIZE = 2^32)
737
+ // and we really want it small enough that we run at least 1 successful target opcode.
738
+ setup: [
739
+ {
740
+ offset: 0,
741
+ value: new Uint32(1000n)
742
+ },
743
+ {
744
+ offset: 1,
745
+ value: new Uint32(0n)
746
+ }
747
+ ],
748
+ targetInstructions: ()=>[
749
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*cdStartOffset=*/ 1, /*dstOffset=*/ 2)
750
+ ]
751
+ },
752
+ {
753
+ label: 'Near min copy size of 1',
754
+ // Near-min but actually copies data (more meaningful than size=0 no-op)
755
+ setup: [
756
+ {
757
+ offset: 0,
758
+ value: new Uint32(1n)
759
+ },
760
+ {
761
+ offset: 1,
762
+ value: new Uint32(0n)
763
+ }
764
+ ],
765
+ targetInstructions: ()=>[
766
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*cdStartOffset=*/ 1, /*dstOffset=*/ 2)
767
+ ]
768
+ }
769
+ ],
770
+ [Opcode.SUCCESSCOPY]: [
771
+ {
772
+ setup: [],
773
+ targetInstructions: ()=>[
774
+ new SuccessCopy(/*indirect=*/ 0, /*dstOffset=*/ 0)
775
+ ]
776
+ }
777
+ ],
778
+ [Opcode.RETURNDATASIZE]: [
779
+ {
780
+ setup: [],
781
+ targetInstructions: ()=>[
782
+ new ReturndataSize(/*indirect=*/ 0, /*dstOffset=*/ 0)
783
+ ]
784
+ }
785
+ ],
786
+ // RETURNDATACOPY has dynamic gas scaling with copySize
787
+ [Opcode.RETURNDATACOPY]: [
788
+ {
789
+ label: 'Min copy size',
790
+ setup: [
791
+ {
792
+ offset: 0,
793
+ value: new Uint32(0n)
794
+ },
795
+ {
796
+ offset: 1,
797
+ value: new Uint32(0n)
798
+ }
799
+ ],
800
+ targetInstructions: ()=>[
801
+ new ReturndataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*rdStartOffset=*/ 1, /*dstOffset=*/ 2)
802
+ ]
803
+ },
804
+ {
805
+ label: 'Large copy size',
806
+ // Large copySize to maximize dynamic gas - will OOG quickly
807
+ // NOTE: we don't want it so large that it exceeds memory bounds (MAX_MEMORY_SIZE = 2^32)
808
+ // and we really want it small enough that we run at least 1 successful target opcode.
809
+ setup: [
810
+ {
811
+ offset: 0,
812
+ value: new Uint32(1000n)
813
+ },
814
+ {
815
+ offset: 1,
816
+ value: new Uint32(0n)
817
+ }
818
+ ],
819
+ targetInstructions: ()=>[
820
+ new ReturndataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*rdStartOffset=*/ 1, /*dstOffset=*/ 2)
821
+ ]
822
+ },
823
+ {
824
+ label: 'Near min copy size of 1',
825
+ // Near-min but actually copies data (more meaningful than size=0 no-op)
826
+ setup: [
827
+ {
828
+ offset: 0,
829
+ value: new Uint32(1n)
830
+ },
831
+ {
832
+ offset: 1,
833
+ value: new Uint32(0n)
834
+ }
835
+ ],
836
+ targetInstructions: ()=>[
837
+ new ReturndataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*rdStartOffset=*/ 1, /*dstOffset=*/ 2)
838
+ ]
839
+ }
840
+ ],
841
+ // ═══════════════════════════════════════════════════════════════════════════
842
+ // WORLD STATE READS
843
+ // ═══════════════════════════════════════════════════════════════════════════
844
+ [Opcode.SLOAD]: [
845
+ {
846
+ label: 'Cold read (slot not written)',
847
+ setup: [
848
+ {
849
+ offset: 0,
850
+ value: new Field(Fr.random())
851
+ }
852
+ ],
853
+ targetInstructions: ()=>[
854
+ new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*dstOffset=*/ 1)
855
+ ]
856
+ },
857
+ {
858
+ label: 'Warm read (SSTORE first)',
859
+ // Memory layout: slot (incremented), value, constant 1, revertSize, loaded value
860
+ setup: [
861
+ {
862
+ offset: 0,
863
+ value: new Field(Fr.random())
864
+ },
865
+ {
866
+ offset: 1,
867
+ value: new Field(Fr.random())
868
+ },
869
+ {
870
+ offset: 2,
871
+ value: new Field(1n)
872
+ },
873
+ {
874
+ offset: 3,
875
+ value: new Uint32(0n)
876
+ }
877
+ ],
878
+ targetInstructions: ()=>[
879
+ new SStore(/*indirect=*/ 0, /*srcOffset=*/ 1, /*slotOffset=*/ 0),
880
+ new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*dstOffset=*/ 4),
881
+ new Add(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 0).as(Opcode.ADD_8, Add.wireFormat8)
882
+ ],
883
+ cleanupInstructions: ()=>[
884
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 3, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
885
+ ],
886
+ limit: MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX
887
+ }
888
+ ],
889
+ [Opcode.NOTEHASHEXISTS]: [
890
+ {
891
+ // Note: Can't easily do "write first" version - would need to know the leaf index
892
+ // that EMITNOTEHASH will produce, which depends on tree state
893
+ setup: [
894
+ {
895
+ offset: 0,
896
+ value: new Field(Fr.random())
897
+ },
898
+ {
899
+ offset: 1,
900
+ value: randomWithTag(TypeTag.UINT64)
901
+ }
902
+ ],
903
+ targetInstructions: ()=>[
904
+ new NoteHashExists(/*indirect=*/ 0, /*noteHashOffset=*/ 0, /*leafIndexOffset=*/ 1, /*existsOffset=*/ 2)
905
+ ]
906
+ }
907
+ ],
908
+ [Opcode.NULLIFIEREXISTS]: [
909
+ {
910
+ label: 'Non-existent nullifier',
911
+ setup: [
912
+ {
913
+ offset: 0,
914
+ value: new Field(Fr.random())
915
+ },
916
+ {
917
+ offset: 1,
918
+ value: new Field(Fr.random())
919
+ }
920
+ ],
921
+ targetInstructions: ()=>[
922
+ new NullifierExists(/*indirect=*/ 0, /*nullifierOffset=*/ 0, /*addressOffset=*/ 1, /*existsOffset=*/ 2)
923
+ ]
924
+ },
925
+ {
926
+ label: 'Existing nullifier (EMITNULLIFIER first)',
927
+ // Memory layout: nullifier (incremented), constant 1, current address (from GETENVVAR), revertSize, exists result
928
+ setup: [
929
+ {
930
+ offset: 0,
931
+ value: new Field(Fr.random())
932
+ },
933
+ {
934
+ offset: 1,
935
+ value: new Field(1n)
936
+ },
937
+ ()=>[
938
+ // Get current contract address into offset 2
939
+ new GetEnvVar(/*indirect=*/ 0, /*dstOffset=*/ 2, /*varEnum=*/ 0).as(Opcode.GETENVVAR_16, GetEnvVar.wireFormat16)
940
+ ],
941
+ {
942
+ offset: 3,
943
+ value: new Uint32(0n)
944
+ }
945
+ ],
946
+ targetInstructions: ()=>[
947
+ new EmitNullifier(/*indirect=*/ 0, /*nullifierOffset=*/ 0),
948
+ new NullifierExists(/*indirect=*/ 0, /*nullifierOffset=*/ 0, /*addressOffset=*/ 2, /*existsOffset=*/ 4),
949
+ new Add(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.ADD_8, Add.wireFormat8)
950
+ ],
951
+ cleanupInstructions: ()=>[
952
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 3, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
953
+ ],
954
+ limit: MAX_NULLIFIERS_PER_TX - 1
955
+ }
956
+ ],
957
+ [Opcode.L1TOL2MSGEXISTS]: [
958
+ {
959
+ setup: [
960
+ {
961
+ offset: 0,
962
+ value: new Field(Fr.random())
963
+ },
964
+ {
965
+ offset: 1,
966
+ value: randomWithTag(TypeTag.UINT64)
967
+ }
968
+ ],
969
+ targetInstructions: ()=>[
970
+ new L1ToL2MessageExists(/*indirect=*/ 0, /*msgHashOffset=*/ 0, /*msgLeafIndexOffset=*/ 1, /*existsOffset=*/ 2)
971
+ ]
972
+ }
973
+ ],
974
+ [Opcode.GETCONTRACTINSTANCE]: [
975
+ {
976
+ // Use GETENVVAR to get current contract address (varEnum 0 = ADDRESS)
977
+ // This ensures we're querying a valid deployed contract
978
+ setup: [
979
+ ()=>[
980
+ new GetEnvVar(/*indirect=*/ 0, /*dstOffset=*/ 0, /*varEnum=*/ 0).as(Opcode.GETENVVAR_16, GetEnvVar.wireFormat16)
981
+ ]
982
+ ],
983
+ // memberEnum 0 = DEPLOYER
984
+ targetInstructions: ()=>[
985
+ new GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1, /*memberEnum=*/ 0)
986
+ ]
987
+ }
988
+ ],
989
+ // ═══════════════════════════════════════════════════════════════════════════
990
+ // SIDE-EFFECT LIMITED (have per-TX limit, use nested call pattern)
991
+ // ═══════════════════════════════════════════════════════════════════════════
992
+ [Opcode.EMITNOTEHASH]: [
993
+ {
994
+ setup: [
995
+ {
996
+ offset: 0,
997
+ value: new Field(Fr.random())
998
+ },
999
+ {
1000
+ offset: 1,
1001
+ value: new Uint32(0n)
1002
+ }
1003
+ ],
1004
+ targetInstructions: ()=>[
1005
+ new EmitNoteHash(/*indirect=*/ 0, /*noteHashOffset=*/ 0)
1006
+ ],
1007
+ cleanupInstructions: ()=>[
1008
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 1, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1009
+ ],
1010
+ limit: MAX_NOTE_HASHES_PER_TX
1011
+ }
1012
+ ],
1013
+ [Opcode.EMITNULLIFIER]: [
1014
+ {
1015
+ // Nullifiers must be unique - increment value after each emit
1016
+ // Memory layout: offset 0 = nullifier value, offset 1 = constant 1 for incrementing
1017
+ setup: [
1018
+ {
1019
+ offset: 0,
1020
+ value: new Field(Fr.random())
1021
+ },
1022
+ {
1023
+ offset: 1,
1024
+ value: new Field(1n)
1025
+ },
1026
+ {
1027
+ offset: 2,
1028
+ value: new Uint32(0n)
1029
+ }
1030
+ ],
1031
+ targetInstructions: ()=>[
1032
+ new EmitNullifier(/*indirect=*/ 0, /*nullifierOffset=*/ 0),
1033
+ new Add(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.ADD_8, Add.wireFormat8)
1034
+ ],
1035
+ cleanupInstructions: ()=>[
1036
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 2, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1037
+ ],
1038
+ limit: MAX_NULLIFIERS_PER_TX - 1
1039
+ }
1040
+ ],
1041
+ [Opcode.SENDL2TOL1MSG]: [
1042
+ {
1043
+ setup: [
1044
+ {
1045
+ offset: 0,
1046
+ value: new Field(Fr.random())
1047
+ },
1048
+ {
1049
+ offset: 1,
1050
+ value: new Field(Fr.random())
1051
+ },
1052
+ {
1053
+ offset: 2,
1054
+ value: new Uint32(0n)
1055
+ }
1056
+ ],
1057
+ targetInstructions: ()=>[
1058
+ new SendL2ToL1Message(/*indirect=*/ 0, /*recipientOffset=*/ 0, /*contentOffset=*/ 1)
1059
+ ],
1060
+ cleanupInstructions: ()=>[
1061
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 2, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1062
+ ],
1063
+ limit: MAX_L2_TO_L1_MSGS_PER_TX
1064
+ }
1065
+ ],
1066
+ // SSTORE has two modes:
1067
+ // 1. Same slot: Writing to the same slot repeatedly has no per-TX limit - it just overwrites.
1068
+ // 2. Unique slots: Writing to unique slots is limited by MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX.
1069
+ [Opcode.SSTORE]: [
1070
+ {
1071
+ label: 'Same slot (no limit)',
1072
+ setup: [
1073
+ {
1074
+ offset: 0,
1075
+ value: new Field(Fr.random())
1076
+ },
1077
+ {
1078
+ offset: 1,
1079
+ value: new Field(Fr.random())
1080
+ },
1081
+ {
1082
+ offset: 2,
1083
+ value: new Uint32(0n)
1084
+ }
1085
+ ],
1086
+ targetInstructions: ()=>[
1087
+ new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*slotOffset=*/ 1)
1088
+ ],
1089
+ cleanupInstructions: ()=>[
1090
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 2, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1091
+ ]
1092
+ },
1093
+ {
1094
+ label: 'Unique slots (side-effect limited)',
1095
+ setup: [
1096
+ {
1097
+ offset: 0,
1098
+ value: new Field(Fr.random())
1099
+ },
1100
+ {
1101
+ offset: 1,
1102
+ value: new Field(Fr.random())
1103
+ },
1104
+ {
1105
+ offset: 2,
1106
+ value: new Field(1n)
1107
+ },
1108
+ {
1109
+ offset: 3,
1110
+ value: new Uint32(0n)
1111
+ }
1112
+ ],
1113
+ targetInstructions: ()=>[
1114
+ new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*slotOffset=*/ 1),
1115
+ new Add(/*indirect=*/ 0, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 1).as(Opcode.ADD_8, Add.wireFormat8)
1116
+ ],
1117
+ cleanupInstructions: ()=>[
1118
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 3, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1119
+ ],
1120
+ limit: MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX
1121
+ }
1122
+ ],
1123
+ // EMITUNENCRYPTEDLOG - two configs: minimal (many small logs) and max-size (one large log)
1124
+ [Opcode.EMITUNENCRYPTEDLOG]: [
1125
+ {
1126
+ label: 'Many empty logs, revert, repeat',
1127
+ setup: [
1128
+ {
1129
+ offset: 0,
1130
+ value: new Uint32(0n)
1131
+ },
1132
+ {
1133
+ offset: 1,
1134
+ value: new Uint32(0n)
1135
+ }
1136
+ ],
1137
+ targetInstructions: ()=>[
1138
+ new EmitUnencryptedLog(/*indirect=*/ 0, /*logSizeOffset=*/ 0, /*logOffset=*/ 1)
1139
+ ],
1140
+ cleanupInstructions: ()=>[
1141
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 1, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1142
+ ],
1143
+ // Max logs with 0-field content: floor(4096 / 2) = 2048
1144
+ limit: Math.floor(FLAT_PUBLIC_LOGS_PAYLOAD_LENGTH / PUBLIC_LOG_HEADER_LENGTH)
1145
+ },
1146
+ {
1147
+ label: 'One max size log, revert, repeat',
1148
+ setup: [
1149
+ // logSize = MAX_PUBLIC_LOG_SIZE_IN_FIELDS
1150
+ {
1151
+ offset: 0,
1152
+ value: new Uint32(BigInt(MAX_PUBLIC_LOG_SIZE_IN_FIELDS))
1153
+ },
1154
+ {
1155
+ offset: 1,
1156
+ value: new Uint32(0n)
1157
+ }
1158
+ ],
1159
+ targetInstructions: ()=>[
1160
+ new EmitUnencryptedLog(/*indirect=*/ 0, /*logSizeOffset=*/ 0, /*logOffset=*/ 2)
1161
+ ],
1162
+ cleanupInstructions: ()=>[
1163
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 1, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8)
1164
+ ],
1165
+ limit: 1
1166
+ }
1167
+ ],
1168
+ // ═══════════════════════════════════════════════════════════════════════════
1169
+ // GADGETS - Random inputs (seeded via SEED env var)
1170
+ // ═══════════════════════════════════════════════════════════════════════════
1171
+ [Opcode.POSEIDON2]: [
1172
+ {
1173
+ // Poseidon2 takes 4 field elements as input
1174
+ setup: Array.from({
1175
+ length: 4
1176
+ }, (_, i)=>({
1177
+ offset: i,
1178
+ value: new Field(Fr.random())
1179
+ })),
1180
+ // Poseidon hash data at M[0..3], write result to M[0:3] (reuse results as next inputs)
1181
+ targetInstructions: ()=>[
1182
+ new Poseidon2(/*indirect=*/ 0, /*inputStateOffset=*/ 0, /*outputStateOffset=*/ 0)
1183
+ ]
1184
+ }
1185
+ ],
1186
+ [Opcode.SHA256COMPRESSION]: [
1187
+ {
1188
+ setup: [
1189
+ // State: 8 x UINT32 at offsets 0-7 (random initial state)
1190
+ ...Array.from({
1191
+ length: 8
1192
+ }, (_, i)=>({
1193
+ offset: i,
1194
+ value: randomWithTag(TypeTag.UINT32)
1195
+ })),
1196
+ // Inputs: 16 x UINT32 at offsets 8-23 (random message block)
1197
+ ...Array.from({
1198
+ length: 16
1199
+ }, (_, i)=>({
1200
+ offset: 8 + i,
1201
+ value: randomWithTag(TypeTag.UINT32)
1202
+ }))
1203
+ ],
1204
+ targetInstructions: ()=>[
1205
+ new Sha256Compression(/*indirect=*/ 0, /*outputOffset=*/ 0, /*stateOffset=*/ 0, /*inputsOffset=*/ 8)
1206
+ ]
1207
+ }
1208
+ ],
1209
+ [Opcode.KECCAKF1600]: [
1210
+ {
1211
+ // Keccak state: 25 x UINT64 (5x5 lane array) with random values
1212
+ setup: Array.from({
1213
+ length: 25
1214
+ }, (_, i)=>({
1215
+ offset: i,
1216
+ value: randomWithTag(TypeTag.UINT64)
1217
+ })),
1218
+ targetInstructions: ()=>[
1219
+ new KeccakF1600(/*indirect=*/ 0, /*dstOffset=*/ 0, /*inputOffset=*/ 0)
1220
+ ]
1221
+ }
1222
+ ],
1223
+ [Opcode.ECADD]: [
1224
+ {
1225
+ // Use the Grumpkin generator point G for both points (valid curve point)
1226
+ setup: [
1227
+ {
1228
+ offset: 0,
1229
+ value: new Field(Grumpkin.generator.x)
1230
+ },
1231
+ {
1232
+ offset: 1,
1233
+ value: new Field(Grumpkin.generator.y)
1234
+ },
1235
+ {
1236
+ offset: 2,
1237
+ value: new Uint1(0n)
1238
+ },
1239
+ {
1240
+ offset: 3,
1241
+ value: new Field(Grumpkin.generator.x)
1242
+ },
1243
+ {
1244
+ offset: 4,
1245
+ value: new Field(Grumpkin.generator.y)
1246
+ },
1247
+ {
1248
+ offset: 5,
1249
+ value: new Uint1(0n)
1250
+ }
1251
+ ],
1252
+ targetInstructions: ()=>[
1253
+ new EcAdd(/*indirect=*/ 0, /*p1XOffset=*/ 0, /*p1YOffset=*/ 1, /*p1IsInfiniteOffset=*/ 2, /*p2XOffset=*/ 3, /*p2YOffset=*/ 4, /*p2IsInfiniteOffset=*/ 5, /*dstOffset=*/ 0)
1254
+ ]
1255
+ }
1256
+ ],
1257
+ // TORADIXBE has dynamic gas scaling with numLimbs
1258
+ [Opcode.TORADIXBE]: [
1259
+ {
1260
+ label: 'Min limbs',
1261
+ setup: [
1262
+ {
1263
+ offset: 0,
1264
+ value: new Field(1n)
1265
+ },
1266
+ {
1267
+ offset: 1,
1268
+ value: new Uint32(2n)
1269
+ },
1270
+ {
1271
+ offset: 2,
1272
+ value: new Uint32(1n)
1273
+ },
1274
+ {
1275
+ offset: 3,
1276
+ value: new Uint1(0n)
1277
+ }
1278
+ ],
1279
+ targetInstructions: ()=>[
1280
+ new ToRadixBE(/*indirect=*/ 0, /*srcOffset=*/ 0, /*radixOffset=*/ 1, /*numLimbsOffset=*/ 2, /*outputBitsOffset=*/ 3, /*dstOffset=*/ 4)
1281
+ ]
1282
+ },
1283
+ {
1284
+ label: 'Max limbs',
1285
+ setup: [
1286
+ {
1287
+ offset: 0,
1288
+ value: new Field(Fr.random())
1289
+ },
1290
+ {
1291
+ offset: 1,
1292
+ value: new Uint32(2n)
1293
+ },
1294
+ {
1295
+ offset: 2,
1296
+ value: new Uint32(256n)
1297
+ },
1298
+ {
1299
+ offset: 3,
1300
+ value: new Uint1(0n)
1301
+ }
1302
+ ],
1303
+ targetInstructions: ()=>[
1304
+ new ToRadixBE(/*indirect=*/ 0, /*srcOffset=*/ 0, /*radixOffset=*/ 1, /*numLimbsOffset=*/ 2, /*outputBitsOffset=*/ 3, /*dstOffset=*/ 4)
1305
+ ]
1306
+ }
1307
+ ],
1308
+ // ═══════════════════════════════════════════════════════════════════════════
1309
+ // MISC
1310
+ // ═══════════════════════════════════════════════════════════════════════════
1311
+ // DEBUGLOG only has base gas (no dynamic gas scaling) - memory reads only happen
1312
+ // when collectDebugLogs config is enabled
1313
+ [Opcode.DEBUGLOG]: [
1314
+ {
1315
+ setup: [
1316
+ {
1317
+ offset: 0,
1318
+ value: new Field(0n)
1319
+ },
1320
+ {
1321
+ offset: 1,
1322
+ value: new Field(0n)
1323
+ },
1324
+ {
1325
+ offset: 2,
1326
+ value: new Field(0n)
1327
+ },
1328
+ {
1329
+ offset: 3,
1330
+ value: new Uint32(0n)
1331
+ }
1332
+ ],
1333
+ // messageSize = 0
1334
+ targetInstructions: ()=>[
1335
+ new DebugLog(/*indirect=*/ 0, /*levelOffset=*/ 0, /*messageOffset=*/ 1, /*fieldsOffset=*/ 2, /*fieldsSizeOffset=*/ 3, /*messageSize=*/ 0)
1336
+ ]
1337
+ }
1338
+ ]
1339
+ };
1340
+ /**
1341
+ * Get all spam test cases grouped by opcode.
1342
+ * This is the main entry point for tests - it handles all the complexity of
1343
+ * type variants, multiple configs, etc.
1344
+ *
1345
+ * Returns hierarchical structure for nested describe blocks in tests.
1346
+ *
1347
+ * @param maxConfigsPerOpcode - Maximum number of configs to include per opcode.
1348
+ * Defaults to Infinity (no limit). Useful for quick
1349
+ * smoke tests where testing all type variants is too slow,
1350
+ * or for proving tests that are inherently slower.
1351
+ */ export function getSpamConfigsPerOpcode(maxConfigsPerOpcode = Infinity) {
1352
+ const groups = [];
1353
+ for (const [opcodeKey, configs] of Object.entries(SPAM_CONFIGS)){
1354
+ const opcode = Opcode[Number(opcodeKey)];
1355
+ if (!configs) {
1356
+ throw new Error(`Opcode ${opcode} listed in spam configs, but empty`);
1357
+ }
1358
+ // Apply the limit to the number of configs per opcode
1359
+ const limitedConfigs = configs.slice(0, maxConfigsPerOpcode);
1360
+ const cases = limitedConfigs.map((config)=>({
1361
+ ...config,
1362
+ // unlabeled configs just get opcode name
1363
+ label: config.label ? `${opcode}/${config.label}` : opcode
1364
+ }));
1365
+ groups.push({
1366
+ opcode: opcode,
1367
+ configs: cases
1368
+ });
1369
+ }
1370
+ return groups;
1371
+ }
1372
+ // ============================================================================
1373
+ // Helper Functions
1374
+ // ============================================================================
1375
+ /**
1376
+ * Create a SET instruction from a MemoryValue.
1377
+ * Chooses smallest SET variant based on offset and value magnitude for optimal bytecode density.
1378
+ */ function createSetInstruction(offset, memValue) {
1379
+ const tag = memValue.getTag();
1380
+ const value = memValue.toBigInt();
1381
+ // SET_8 only supports offset <= 255 and value <= 255
1382
+ if (offset <= 0xff && value <= 0xffn) {
1383
+ return new Set(0, offset, tag, Number(value)).as(Opcode.SET_8, Set.wireFormat8);
1384
+ }
1385
+ // SET_16+ support offset <= 65535
1386
+ if (value <= 0xffffn) {
1387
+ return new Set(0, offset, tag, Number(value)).as(Opcode.SET_16, Set.wireFormat16);
1388
+ }
1389
+ if (value <= 0xffffffffn) {
1390
+ return new Set(0, offset, tag, Number(value)).as(Opcode.SET_32, Set.wireFormat32);
1391
+ }
1392
+ if (value <= 0xffffffffffffffffn) {
1393
+ return new Set(0, offset, tag, value).as(Opcode.SET_64, Set.wireFormat64);
1394
+ }
1395
+ if (value <= 0xffffffffffffffffffffffffffffffffn) {
1396
+ return new Set(0, offset, tag, value).as(Opcode.SET_128, Set.wireFormat128);
1397
+ }
1398
+ return new Set(0, offset, tag, value).as(Opcode.SET_FF, Set.wireFormatFF);
1399
+ }
1400
+ /**
1401
+ * Append (to the instructions array) the SET instructions for the setup.
1402
+ *
1403
+ * @param instructions - the instructions array to append the setup to
1404
+ * @param setup - the setup configuration specifying what SETs to do
1405
+ */ function appendSetupInstructions(instructions, setup) {
1406
+ for (const item of setup){
1407
+ if (typeof item === 'function') {
1408
+ // item is a function that creates setup instructions (like)
1409
+ instructions.push(...item());
1410
+ } else {
1411
+ // MemSetup
1412
+ instructions.push(createSetInstruction(item.offset, item.value));
1413
+ }
1414
+ }
1415
+ }
1416
+ /**
1417
+ * Append (to the instructions array) the target instructions nTimes times.
1418
+ *
1419
+ * @param instructions - the instructions array to append the loop to
1420
+ * @param config - the spam config to use
1421
+ * @param nTimes - the number of times to append the target instructions
1422
+ * @returns the number of target instructions appended
1423
+ */ function appendTargetNTimes(instructions, config, nTimes) {
1424
+ for(let i = 0; i < nTimes; i++){
1425
+ instructions.push(...config.targetInstructions());
1426
+ }
1427
+ }
1428
+ /**
1429
+ * Append (to the instructions array) an infinite loop that maximizes target instruction density.
1430
+ * Fills remaining bytecode space with unrolled target instructions.
1431
+ *
1432
+ * @param instructions - the instructions array to append the loop to
1433
+ * @param config - the spam config to use
1434
+ * @returns the number of target instructions in the loop body
1435
+ */ function appendInfiniteLoop(instructions, config) {
1436
+ const setupBytecode = encodeToBytecode(instructions);
1437
+ const setupSize = setupBytecode.length;
1438
+ // Compute the size of the target instruction(s)
1439
+ const targetSize = encodeToBytecode(config.targetInstructions()).length;
1440
+ // Fill remaining space (loop body) with target instructions
1441
+ const availableForLoopBody = MAX_BYTECODE_BYTES - setupSize - JUMP_SIZE;
1442
+ const numTargetsInLoopBody = Math.floor(availableForLoopBody / targetSize);
1443
+ const loopStartPc = setupSize;
1444
+ appendTargetNTimes(instructions, config, numTargetsInLoopBody);
1445
+ instructions.push(new Jump(loopStartPc)); // JUMP_SIZE (JUMP_32)
1446
+ return numTargetsInLoopBody;
1447
+ }
1448
+ /**
1449
+ * Generate basic opcode spam bytecode from a SpamConfig.
1450
+ * Spams the target instruction(s) in an infinite loop until out-of-gas.
1451
+ */ export function createOpcodeSpamBytecode(config) {
1452
+ assert(config.limit === undefined, 'If config has `limit`, use createSideEffectLimitedSpamInRevertingNestedCall instead');
1453
+ const instructions = [];
1454
+ // 1. Setup memory
1455
+ appendSetupInstructions(instructions, config.setup);
1456
+ // 2. Infinite loop - maximize calls to target until out-of-gas
1457
+ appendInfiniteLoop(instructions, config);
1458
+ return encodeToBytecode(instructions);
1459
+ }
1460
+ /**
1461
+ * Generate a bytecode that spams a side-effect limited opcode #limit times
1462
+ * NOT in a loop, but inline/unrolled. Then revert.
1463
+ *
1464
+ * @param config - the side-effect limited spam config to use
1465
+ * @returns the bytecode for the side-effect limited spam
1466
+ */ export function createSideEffectSpamBytecode(config) {
1467
+ assert(config.limit !== undefined, 'If config has `limit`, use createSideEffectLimitedSpamInRevertingNestedCall instead');
1468
+ const instructions = [];
1469
+ // 1. Setup
1470
+ appendSetupInstructions(instructions, config.setup);
1471
+ // 2. Body - run target instruction(s) #limit times
1472
+ appendTargetNTimes(instructions, config, config.limit);
1473
+ // 3. Cleanup (revert)
1474
+ if (config.cleanupInstructions) {
1475
+ instructions.push(...config.cleanupInstructions());
1476
+ }
1477
+ return encodeToBytecode(instructions);
1478
+ }
1479
+ async function testStandardOpcodeSpam(tester, config, expectToBeTrue) {
1480
+ const bytecode = createOpcodeSpamBytecode(config);
1481
+ const contract = await deployCustomBytecode(bytecode, tester, config.label);
1482
+ // Should we pass the contract address as calldata?
1483
+ const calldata = config.addressAsCalldata ? [
1484
+ contract.address.toField()
1485
+ ] : [];
1486
+ const result = await executeCustomBytecode(contract, tester, config.label, calldata);
1487
+ // should have halted with out of gas
1488
+ expectToBeTrue(!result.revertCode.isOK());
1489
+ const revertReason = result.findRevertReason()?.message.toLowerCase();
1490
+ const allowedReasons = [
1491
+ 'out of gas',
1492
+ 'not enough l2gas'
1493
+ ];
1494
+ // expect the reason to match ONE of the allowed reasons
1495
+ expectToBeTrue(allowedReasons.some((allowedReason)=>revertReason?.includes(allowedReason)));
1496
+ return result;
1497
+ }
1498
+ async function testSideEffectOpcodeSpam(tester, config, expectToBeTrue) {
1499
+ // Inner contract will spam the side-effect limited opcode up to its limit, then REVERT
1500
+ const innerBytecode = createSideEffectSpamBytecode(config);
1501
+ // Outer contract will CALL to inner contract in a loop
1502
+ const outerBytecode = createOpcodeSpamBytecode(EXTERNAL_CALL_CONFIG);
1503
+ const innerContract = await deployCustomBytecode(innerBytecode, tester, `${config.label}_Inner`);
1504
+ const outerContract = await deployCustomBytecode(outerBytecode, tester, `${config.label}_Outer`);
1505
+ // Outer contract reads calldata[0] as inner contract address to CALL to
1506
+ const result = await executeCustomBytecode(outerContract, tester, config.label, [
1507
+ innerContract.address.toField()
1508
+ ]);
1509
+ // should have halted with out of gas or explicit REVERT (assertion failed)
1510
+ expectToBeTrue(!result.revertCode.isOK());
1511
+ const revertReason = result.findRevertReason()?.message.toLowerCase();
1512
+ const allowedReasons = [
1513
+ 'assertion failed',
1514
+ 'out of gas',
1515
+ 'not enough l2gas'
1516
+ ];
1517
+ // expect the reason to match ONE of the allowed reasons
1518
+ expectToBeTrue(allowedReasons.some((allowedReason)=>revertReason?.includes(allowedReason)));
1519
+ // Top-level should _always_ run out of gas for these tests
1520
+ // Check top-level halting message
1521
+ // WARNING: only the C++ simulator (or TsVsCpp) will have haltingMessage
1522
+ const allowedOuterReasons = [
1523
+ 'out of gas',
1524
+ 'not enough l2gas'
1525
+ ];
1526
+ if (result.callStackMetadata && result.callStackMetadata.length > 0) {
1527
+ const outerCallMetadata = result.callStackMetadata[0];
1528
+ const outerReason = outerCallMetadata.haltingMessage?.toLowerCase();
1529
+ // expect the reason to match ONE of the allowed reasons
1530
+ expectToBeTrue(allowedOuterReasons.some((allowedReason)=>outerReason?.includes(allowedReason)));
1531
+ }
1532
+ return result;
1533
+ }
1534
+ export async function testOpcodeSpamCase(tester, config, expectToBeTrue = ()=>{}) {
1535
+ if (config.limit) {
1536
+ return await testSideEffectOpcodeSpam(tester, config, expectToBeTrue);
1537
+ }
1538
+ return await testStandardOpcodeSpam(tester, config, expectToBeTrue);
1539
+ }