@aztec/simulator 3.0.0-nightly.20251217 → 3.0.0-nightly.20251219

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.
@@ -149,14 +149,14 @@
149
149
  *
150
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
151
  import { Grumpkin } from '@aztec/foundation/crypto/grumpkin';
152
+ import { randomBigInt } from '@aztec/foundation/crypto/random';
152
153
  import { Fr } from '@aztec/foundation/curves/bn254';
153
154
  import assert from 'assert';
154
- import { Field, INTEGRAL_TAGS, TaggedMemory, TypeTag, Uint1, Uint32, Uint64, VALID_TAGS } from '../avm/avm_memory_types.js';
155
- import { Add, And, Call, CalldataCopy, Cast, DebugLog, Div, EcAdd, EmitNoteHash, EmitNullifier, EmitUnencryptedLog, Eq, FieldDiv, GetContractInstance, GetEnvVar, Jump, JumpI, KeccakF1600, L1ToL2MessageExists, Lt, Lte, Mov, Mul, Not, NoteHashExists, NullifierExists, Or, Poseidon2, ReturndataCopy, ReturndataSize, Revert, SLoad, SStore, SendL2ToL1Message, Set, Sha256Compression, Shl, Shr, Sub, SuccessCopy, ToRadixBE, Xor } from '../avm/opcodes/index.js';
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';
156
157
  import { encodeToBytecode } from '../avm/serialization/bytecode_serialization.js';
157
158
  import { Opcode } from '../avm/serialization/instruction_serialization.js';
158
159
  import { deployCustomBytecode, executeCustomBytecode } from './custom_bytecode_tester.js';
159
- import { deployAndExecuteCustomBytecode } from './index.js';
160
160
  // ============================================================================
161
161
  // Constants
162
162
  // ============================================================================
@@ -175,15 +175,131 @@ const MAX_BYTECODE_BYTES = (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * BYT
175
175
  const JUMP_SIZE = encodeToBytecode([
176
176
  new Jump(0)
177
177
  ]).length; // JUMP_32
178
+ const INTERNALCALL_SIZE = encodeToBytecode([
179
+ new InternalCall(0)
180
+ ]).length;
178
181
  // ============================================================================
179
182
  // Type Variant Helpers (for generating multiple configs per opcode)
180
183
  // ============================================================================
181
- const ALL_TAGS = Array.from(VALID_TAGS);
182
- const INT_TAGS = Array.from(INTEGRAL_TAGS);
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
+ ];
183
206
  /** Build from tag truncating - shorter name */ function withTag(v, tag) {
184
207
  return TaggedMemory.buildFromTagTruncating(v, tag);
185
208
  }
186
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
+ // ============================================================================
187
303
  // Configuration Map
188
304
  // ============================================================================
189
305
  /**
@@ -192,18 +308,18 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
192
308
  * Uses smallest wire format (_8) for maximum instruction density.
193
309
  */ export const SPAM_CONFIGS = {
194
310
  // ═══════════════════════════════════════════════════════════════════════════
195
- // ARITHMETIC - Test with all type variants
311
+ // ARITHMETIC - Test with all type variants (random values, seeded via SEED env var)
196
312
  // ═══════════════════════════════════════════════════════════════════════════
197
313
  [Opcode.ADD_8]: ALL_TAGS.map((tag)=>({
198
314
  label: TypeTag[tag],
199
315
  setup: [
200
316
  {
201
317
  offset: 0,
202
- value: withTag(1n, tag)
318
+ value: randomWithTag(tag)
203
319
  },
204
320
  {
205
321
  offset: 1,
206
- value: withTag(1n, tag)
322
+ value: randomWithTag(tag)
207
323
  }
208
324
  ],
209
325
  targetInstructions: ()=>[
@@ -215,11 +331,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
215
331
  setup: [
216
332
  {
217
333
  offset: 0,
218
- value: withTag(1000000n, tag)
334
+ value: randomWithTag(tag)
219
335
  },
220
336
  {
221
337
  offset: 1,
222
- value: withTag(1n, tag)
338
+ value: randomWithTag(tag)
223
339
  }
224
340
  ],
225
341
  targetInstructions: ()=>[
@@ -231,11 +347,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
231
347
  setup: [
232
348
  {
233
349
  offset: 0,
234
- value: withTag(2n, tag)
350
+ value: randomWithTag(tag)
235
351
  },
236
352
  {
237
353
  offset: 1,
238
- value: withTag(2n, tag)
354
+ value: randomWithTag(tag)
239
355
  }
240
356
  ],
241
357
  targetInstructions: ()=>[
@@ -248,11 +364,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
248
364
  setup: [
249
365
  {
250
366
  offset: 0,
251
- value: withTag(111111111111111111n, tag)
367
+ value: randomWithTag(tag)
252
368
  },
253
369
  {
254
370
  offset: 1,
255
- value: withTag(1n, tag)
371
+ value: randomNonZeroWithTag(tag)
256
372
  }
257
373
  ],
258
374
  targetInstructions: ()=>[
@@ -265,11 +381,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
265
381
  setup: [
266
382
  {
267
383
  offset: 0,
268
- value: new Field(1000000n)
384
+ value: new Field(Fr.random())
269
385
  },
270
386
  {
271
387
  offset: 1,
272
- value: new Field(1n)
388
+ value: randomNonZeroField()
273
389
  }
274
390
  ],
275
391
  targetInstructions: ()=>[
@@ -278,18 +394,18 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
278
394
  }
279
395
  ],
280
396
  // ═══════════════════════════════════════════════════════════════════════════
281
- // COMPARATORS - Test with all type variants
397
+ // COMPARATORS - Test with all type variants (random values)
282
398
  // ═══════════════════════════════════════════════════════════════════════════
283
399
  [Opcode.EQ_8]: ALL_TAGS.map((tag)=>({
284
400
  label: TypeTag[tag],
285
401
  setup: [
286
402
  {
287
403
  offset: 0,
288
- value: withTag(42n, tag)
404
+ value: randomWithTag(tag)
289
405
  },
290
406
  {
291
407
  offset: 1,
292
- value: withTag(42n, tag)
408
+ value: randomWithTag(tag)
293
409
  }
294
410
  ],
295
411
  targetInstructions: ()=>[
@@ -301,11 +417,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
301
417
  setup: [
302
418
  {
303
419
  offset: 0,
304
- value: withTag(1n, tag)
420
+ value: randomWithTag(tag)
305
421
  },
306
422
  {
307
423
  offset: 1,
308
- value: withTag(1000000n, tag)
424
+ value: randomWithTag(tag)
309
425
  }
310
426
  ],
311
427
  targetInstructions: ()=>[
@@ -317,11 +433,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
317
433
  setup: [
318
434
  {
319
435
  offset: 0,
320
- value: withTag(1n, tag)
436
+ value: randomWithTag(tag)
321
437
  },
322
438
  {
323
439
  offset: 1,
324
- value: withTag(1000000n, tag)
440
+ value: randomWithTag(tag)
325
441
  }
326
442
  ],
327
443
  targetInstructions: ()=>[
@@ -329,18 +445,18 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
329
445
  ]
330
446
  })),
331
447
  // ═══════════════════════════════════════════════════════════════════════════
332
- // BITWISE - Integer types only (no FIELD)
448
+ // BITWISE - Integer types only (no FIELD) (random values)
333
449
  // ═══════════════════════════════════════════════════════════════════════════
334
450
  [Opcode.AND_8]: INT_TAGS.map((tag)=>({
335
451
  label: TypeTag[tag],
336
452
  setup: [
337
453
  {
338
454
  offset: 0,
339
- value: withTag(0xffffffffffffffffffffffffffffffffn, tag)
455
+ value: randomWithTag(tag)
340
456
  },
341
457
  {
342
458
  offset: 1,
343
- value: withTag(0xffffffffffffffffffffffffffffffffn, tag)
459
+ value: randomWithTag(tag)
344
460
  }
345
461
  ],
346
462
  targetInstructions: ()=>[
@@ -352,11 +468,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
352
468
  setup: [
353
469
  {
354
470
  offset: 0,
355
- value: withTag(0xffffffffffffffffffffffffffffffffn, tag)
471
+ value: randomWithTag(tag)
356
472
  },
357
473
  {
358
474
  offset: 1,
359
- value: withTag(0n, tag)
475
+ value: randomWithTag(tag)
360
476
  }
361
477
  ],
362
478
  targetInstructions: ()=>[
@@ -368,11 +484,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
368
484
  setup: [
369
485
  {
370
486
  offset: 0,
371
- value: withTag(0xdeadbeefcafebaben, tag)
487
+ value: randomWithTag(tag)
372
488
  },
373
489
  {
374
490
  offset: 1,
375
- value: withTag(0x1234567890abcdefn, tag)
491
+ value: randomWithTag(tag)
376
492
  }
377
493
  ],
378
494
  targetInstructions: ()=>[
@@ -384,7 +500,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
384
500
  setup: [
385
501
  {
386
502
  offset: 0,
387
- value: withTag(0xffffffffffffffffn, tag)
503
+ value: randomWithTag(tag)
388
504
  }
389
505
  ],
390
506
  targetInstructions: ()=>[
@@ -396,7 +512,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
396
512
  setup: [
397
513
  {
398
514
  offset: 0,
399
- value: withTag(1n, tag)
515
+ value: randomWithTag(tag)
400
516
  },
401
517
  {
402
518
  offset: 1,
@@ -412,7 +528,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
412
528
  setup: [
413
529
  {
414
530
  offset: 0,
415
- value: withTag(0xffffffffffffffffn, tag)
531
+ value: randomWithTag(tag)
416
532
  },
417
533
  {
418
534
  offset: 1,
@@ -424,14 +540,14 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
424
540
  ]
425
541
  })),
426
542
  // ═══════════════════════════════════════════════════════════════════════════
427
- // CAST / MOV - Test with all type variants
543
+ // CAST / MOV - Test with all type variants (random values)
428
544
  // ═══════════════════════════════════════════════════════════════════════════
429
545
  [Opcode.CAST_8]: ALL_TAGS.map((tag)=>({
430
546
  label: TypeTag[tag],
431
547
  setup: [
432
548
  {
433
549
  offset: 0,
434
- value: withTag(42n, tag)
550
+ value: randomWithTag(tag)
435
551
  }
436
552
  ],
437
553
  targetInstructions: ()=>[
@@ -443,7 +559,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
443
559
  setup: [
444
560
  {
445
561
  offset: 0,
446
- value: withTag(42n, tag)
562
+ value: randomWithTag(tag)
447
563
  }
448
564
  ],
449
565
  targetInstructions: ()=>[
@@ -518,6 +634,72 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
518
634
  ]
519
635
  }
520
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
+ ],
521
703
  // ═══════════════════════════════════════════════════════════════════════════
522
704
  // ENVIRONMENT
523
705
  // ═══════════════════════════════════════════════════════════════════════════
@@ -529,11 +711,47 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
529
711
  ]
530
712
  }
531
713
  ],
714
+ // CALLDATACOPY has dynamic gas scaling with copySize
532
715
  [Opcode.CALLDATACOPY]: [
533
716
  {
534
- // CalldataCopy(indirect=0, copySizeOffset=0, cdStartOffset=1, dstOffset=2)
535
- // Copies M[0]=1 elements starting from CD[M[1]]=CD[0] into M[2].
536
- // In other words: M[2] = CD[0]
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)
537
755
  setup: [
538
756
  {
539
757
  offset: 0,
@@ -565,8 +783,10 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
565
783
  ]
566
784
  }
567
785
  ],
786
+ // RETURNDATACOPY has dynamic gas scaling with copySize
568
787
  [Opcode.RETURNDATACOPY]: [
569
788
  {
789
+ label: 'Min copy size',
570
790
  setup: [
571
791
  {
572
792
  offset: 0,
@@ -580,6 +800,42 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
580
800
  targetInstructions: ()=>[
581
801
  new ReturndataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*rdStartOffset=*/ 1, /*dstOffset=*/ 2)
582
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
+ ]
583
839
  }
584
840
  ],
585
841
  // ═══════════════════════════════════════════════════════════════════════════
@@ -587,27 +843,61 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
587
843
  // ═══════════════════════════════════════════════════════════════════════════
588
844
  [Opcode.SLOAD]: [
589
845
  {
846
+ label: 'Cold read (slot not written)',
590
847
  setup: [
591
848
  {
592
849
  offset: 0,
593
- value: new Field(0n)
850
+ value: new Field(Fr.random())
594
851
  }
595
852
  ],
596
853
  targetInstructions: ()=>[
597
854
  new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*dstOffset=*/ 1)
598
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
599
887
  }
600
888
  ],
601
889
  [Opcode.NOTEHASHEXISTS]: [
602
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
603
893
  setup: [
604
894
  {
605
895
  offset: 0,
606
- value: new Field(0n)
896
+ value: new Field(Fr.random())
607
897
  },
608
898
  {
609
899
  offset: 1,
610
- value: new Uint64(0n)
900
+ value: randomWithTag(TypeTag.UINT64)
611
901
  }
612
902
  ],
613
903
  targetInstructions: ()=>[
@@ -617,19 +907,51 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
617
907
  ],
618
908
  [Opcode.NULLIFIEREXISTS]: [
619
909
  {
910
+ label: 'Non-existent nullifier',
620
911
  setup: [
621
912
  {
622
913
  offset: 0,
623
- value: new Field(0n)
914
+ value: new Field(Fr.random())
624
915
  },
625
916
  {
626
917
  offset: 1,
627
- value: new Field(0n)
918
+ value: new Field(Fr.random())
628
919
  }
629
920
  ],
630
921
  targetInstructions: ()=>[
631
922
  new NullifierExists(/*indirect=*/ 0, /*nullifierOffset=*/ 0, /*addressOffset=*/ 1, /*existsOffset=*/ 2)
632
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
633
955
  }
634
956
  ],
635
957
  [Opcode.L1TOL2MSGEXISTS]: [
@@ -637,11 +959,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
637
959
  setup: [
638
960
  {
639
961
  offset: 0,
640
- value: new Field(0n)
962
+ value: new Field(Fr.random())
641
963
  },
642
964
  {
643
965
  offset: 1,
644
- value: new Uint64(0n)
966
+ value: randomWithTag(TypeTag.UINT64)
645
967
  }
646
968
  ],
647
969
  targetInstructions: ()=>[
@@ -651,11 +973,12 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
651
973
  ],
652
974
  [Opcode.GETCONTRACTINSTANCE]: [
653
975
  {
976
+ // Use GETENVVAR to get current contract address (varEnum 0 = ADDRESS)
977
+ // This ensures we're querying a valid deployed contract
654
978
  setup: [
655
- {
656
- offset: 0,
657
- value: new Field(0n)
658
- }
979
+ ()=>[
980
+ new GetEnvVar(/*indirect=*/ 0, /*dstOffset=*/ 0, /*varEnum=*/ 0).as(Opcode.GETENVVAR_16, GetEnvVar.wireFormat16)
981
+ ]
659
982
  ],
660
983
  // memberEnum 0 = DEPLOYER
661
984
  targetInstructions: ()=>[
@@ -671,7 +994,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
671
994
  setup: [
672
995
  {
673
996
  offset: 0,
674
- value: new Field(0x1000n)
997
+ value: new Field(Fr.random())
675
998
  },
676
999
  {
677
1000
  offset: 1,
@@ -694,7 +1017,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
694
1017
  setup: [
695
1018
  {
696
1019
  offset: 0,
697
- value: new Field(0x2000n)
1020
+ value: new Field(Fr.random())
698
1021
  },
699
1022
  {
700
1023
  offset: 1,
@@ -720,11 +1043,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
720
1043
  setup: [
721
1044
  {
722
1045
  offset: 0,
723
- value: new Field(1n)
1046
+ value: new Field(Fr.random())
724
1047
  },
725
1048
  {
726
1049
  offset: 1,
727
- value: new Field(0x3000n)
1050
+ value: new Field(Fr.random())
728
1051
  },
729
1052
  {
730
1053
  offset: 2,
@@ -749,11 +1072,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
749
1072
  setup: [
750
1073
  {
751
1074
  offset: 0,
752
- value: new Field(42n)
1075
+ value: new Field(Fr.random())
753
1076
  },
754
1077
  {
755
1078
  offset: 1,
756
- value: new Field(0x100n)
1079
+ value: new Field(Fr.random())
757
1080
  },
758
1081
  {
759
1082
  offset: 2,
@@ -772,11 +1095,11 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
772
1095
  setup: [
773
1096
  {
774
1097
  offset: 0,
775
- value: new Field(42n)
1098
+ value: new Field(Fr.random())
776
1099
  },
777
1100
  {
778
1101
  offset: 1,
779
- value: new Field(0x100n)
1102
+ value: new Field(Fr.random())
780
1103
  },
781
1104
  {
782
1105
  offset: 2,
@@ -843,7 +1166,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
843
1166
  }
844
1167
  ],
845
1168
  // ═══════════════════════════════════════════════════════════════════════════
846
- // GADGETS - Use non-trivial inputs to avoid special-case optimizations
1169
+ // GADGETS - Random inputs (seeded via SEED env var)
847
1170
  // ═══════════════════════════════════════════════════════════════════════════
848
1171
  [Opcode.POSEIDON2]: [
849
1172
  {
@@ -852,7 +1175,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
852
1175
  length: 4
853
1176
  }, (_, i)=>({
854
1177
  offset: i,
855
- value: new Field(BigInt(0xdeadbeef + i * 0x1111))
1178
+ value: new Field(Fr.random())
856
1179
  })),
857
1180
  // Poseidon hash data at M[0..3], write result to M[0:3] (reuse results as next inputs)
858
1181
  targetInstructions: ()=>[
@@ -863,45 +1186,19 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
863
1186
  [Opcode.SHA256COMPRESSION]: [
864
1187
  {
865
1188
  setup: [
866
- // State: 8 x UINT32 at offsets 0-7 (use SHA256 initial hash values)
867
- {
868
- offset: 0,
869
- value: new Uint32(0x6a09e667n)
870
- },
871
- {
872
- offset: 1,
873
- value: new Uint32(0xbb67ae85n)
874
- },
875
- {
876
- offset: 2,
877
- value: new Uint32(0x3c6ef372n)
878
- },
879
- {
880
- offset: 3,
881
- value: new Uint32(0xa54ff53an)
882
- },
883
- {
884
- offset: 4,
885
- value: new Uint32(0x510e527fn)
886
- },
887
- {
888
- offset: 5,
889
- value: new Uint32(0x9b05688cn)
890
- },
891
- {
892
- offset: 6,
893
- value: new Uint32(0x1f83d9abn)
894
- },
895
- {
896
- offset: 7,
897
- value: new Uint32(0x5be0cd19n)
898
- },
899
- // Inputs: 16 x UINT32 at offsets 8-23 (non-trivial message block)
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)
900
1197
  ...Array.from({
901
1198
  length: 16
902
1199
  }, (_, i)=>({
903
1200
  offset: 8 + i,
904
- value: new Uint32(0xcafebaben + BigInt(i) * 0x01010101n & 0xffffffffn)
1201
+ value: randomWithTag(TypeTag.UINT32)
905
1202
  }))
906
1203
  ],
907
1204
  targetInstructions: ()=>[
@@ -911,12 +1208,12 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
911
1208
  ],
912
1209
  [Opcode.KECCAKF1600]: [
913
1210
  {
914
- // Keccak state: 25 x UINT64 (5x5 lane array)
1211
+ // Keccak state: 25 x UINT64 (5x5 lane array) with random values
915
1212
  setup: Array.from({
916
1213
  length: 25
917
1214
  }, (_, i)=>({
918
1215
  offset: i,
919
- value: new Uint64(0xdeadbeefcafebaben + BigInt(i) * 0x0101010101010101n & 0xffffffffffffffffn)
1216
+ value: randomWithTag(TypeTag.UINT64)
920
1217
  })),
921
1218
  targetInstructions: ()=>[
922
1219
  new KeccakF1600(/*indirect=*/ 0, /*dstOffset=*/ 0, /*inputOffset=*/ 0)
@@ -957,20 +1254,46 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
957
1254
  ]
958
1255
  }
959
1256
  ],
1257
+ // TORADIXBE has dynamic gas scaling with numLimbs
960
1258
  [Opcode.TORADIXBE]: [
961
1259
  {
1260
+ label: 'Min limbs',
962
1261
  setup: [
963
1262
  {
964
1263
  offset: 0,
965
- value: new Field(0xdeadbeefcafebaben)
1264
+ value: new Field(1n)
966
1265
  },
967
1266
  {
968
1267
  offset: 1,
969
- value: new Uint32(16n)
1268
+ value: new Uint32(2n)
970
1269
  },
971
1270
  {
972
1271
  offset: 2,
973
- value: new Uint32(16n)
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)
974
1297
  },
975
1298
  {
976
1299
  offset: 3,
@@ -985,6 +1308,8 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
985
1308
  // ═══════════════════════════════════════════════════════════════════════════
986
1309
  // MISC
987
1310
  // ═══════════════════════════════════════════════════════════════════════════
1311
+ // DEBUGLOG only has base gas (no dynamic gas scaling) - memory reads only happen
1312
+ // when collectDebugLogs config is enabled
988
1313
  [Opcode.DEBUGLOG]: [
989
1314
  {
990
1315
  setup: [
@@ -1128,7 +1453,7 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
1128
1453
  const instructions = [];
1129
1454
  // 1. Setup memory
1130
1455
  appendSetupInstructions(instructions, config.setup);
1131
- // 2. Infinite loop - maximize iterations until out-of-gas
1456
+ // 2. Infinite loop - maximize calls to target until out-of-gas
1132
1457
  appendInfiniteLoop(instructions, config);
1133
1458
  return encodeToBytecode(instructions);
1134
1459
  }
@@ -1151,66 +1476,14 @@ const INT_TAGS = Array.from(INTEGRAL_TAGS);
1151
1476
  }
1152
1477
  return encodeToBytecode(instructions);
1153
1478
  }
1154
- /** Reserved memory offsets for outer call loop */ const CONST_1_OFFSET = 0;
1155
- const CALL_L2_GAS_OFFSET = 1;
1156
- const CALL_DA_GAS_OFFSET = 2;
1157
- const CALL_ADDR_OFFSET = 3;
1158
- const CALL_ARGS_SIZE = 4;
1159
- const CALL_ARGS_OFFSET = 5;
1160
- const CALLDATA_INDEX_OFFSET = 6; // calldata index as in calldata[index]
1161
- /**
1162
- * A SpamConfig for an external call loop.
1163
- */ const EXTERNAL_CALL_LOOP_CONFIG = {
1164
- setup: [
1165
- // calldata will contain 1 item: the external call address
1166
- {
1167
- offset: CONST_1_OFFSET,
1168
- value: new Uint32(1)
1169
- },
1170
- {
1171
- offset: CALLDATA_INDEX_OFFSET,
1172
- value: new Uint32(0)
1173
- },
1174
- {
1175
- offset: CALL_L2_GAS_OFFSET,
1176
- value: new Uint32(0xffffffffn)
1177
- },
1178
- {
1179
- offset: CALL_DA_GAS_OFFSET,
1180
- value: new Uint32(0xffffffffn)
1181
- },
1182
- ()=>[
1183
- new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ CONST_1_OFFSET, /*cdStartOffset=*/ CALLDATA_INDEX_OFFSET, /*dstOffset=*/ CALL_ADDR_OFFSET)
1184
- ],
1185
- {
1186
- offset: CALL_ARGS_SIZE,
1187
- value: new Uint32(0)
1188
- },
1189
- {
1190
- offset: CALL_ARGS_SIZE,
1191
- value: new Uint32(0)
1192
- }
1193
- ],
1194
- targetInstructions: ()=>[
1195
- new Call(/*indirect=*/ 0, /*l2GasOffset=*/ CALL_L2_GAS_OFFSET, /*daGasOffset=*/ CALL_DA_GAS_OFFSET, /*addrOffset=*/ CALL_ADDR_OFFSET, /*argsSizeOffset=*/ CALL_ARGS_SIZE, /*argsOffset=*/ CALL_ARGS_OFFSET)
1196
- ]
1197
- };
1198
- /**
1199
- * Create bytecode that makes an external call in a loop.
1200
- *
1201
- * @returns the bytecode for the external call loop
1202
- */ export function createExternalCallLoopBytecode() {
1203
- const config = EXTERNAL_CALL_LOOP_CONFIG;
1204
- const instructions = [];
1205
- // 1. Setup memory
1206
- appendSetupInstructions(instructions, config.setup);
1207
- // 2. Infinite loop of external calls - maximize iterations until out-of-gas
1208
- appendInfiniteLoop(instructions, config);
1209
- return encodeToBytecode(instructions);
1210
- }
1211
1479
  async function testStandardOpcodeSpam(tester, config, expectToBeTrue) {
1212
1480
  const bytecode = createOpcodeSpamBytecode(config);
1213
- const result = await deployAndExecuteCustomBytecode(bytecode, tester, config.label);
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);
1214
1487
  // should have halted with out of gas
1215
1488
  expectToBeTrue(!result.revertCode.isOK());
1216
1489
  const revertReason = result.findRevertReason()?.message.toLowerCase();
@@ -1219,12 +1492,14 @@ async function testStandardOpcodeSpam(tester, config, expectToBeTrue) {
1219
1492
  'not enough l2gas'
1220
1493
  ];
1221
1494
  // expect the reason to match ONE of the allowed reasons
1222
- expectToBeTrue(allowedReasons.some((allowedReason)=>revertReason.includes(allowedReason)));
1495
+ expectToBeTrue(allowedReasons.some((allowedReason)=>revertReason?.includes(allowedReason)));
1223
1496
  return result;
1224
1497
  }
1225
1498
  async function testSideEffectOpcodeSpam(tester, config, expectToBeTrue) {
1499
+ // Inner contract will spam the side-effect limited opcode up to its limit, then REVERT
1226
1500
  const innerBytecode = createSideEffectSpamBytecode(config);
1227
- const outerBytecode = createExternalCallLoopBytecode();
1501
+ // Outer contract will CALL to inner contract in a loop
1502
+ const outerBytecode = createOpcodeSpamBytecode(EXTERNAL_CALL_CONFIG);
1228
1503
  const innerContract = await deployCustomBytecode(innerBytecode, tester, `${config.label}_Inner`);
1229
1504
  const outerContract = await deployCustomBytecode(outerBytecode, tester, `${config.label}_Outer`);
1230
1505
  // Outer contract reads calldata[0] as inner contract address to CALL to
@@ -1240,7 +1515,7 @@ async function testSideEffectOpcodeSpam(tester, config, expectToBeTrue) {
1240
1515
  'not enough l2gas'
1241
1516
  ];
1242
1517
  // expect the reason to match ONE of the allowed reasons
1243
- expectToBeTrue(allowedReasons.some((allowedReason)=>revertReason.includes(allowedReason)));
1518
+ expectToBeTrue(allowedReasons.some((allowedReason)=>revertReason?.includes(allowedReason)));
1244
1519
  // Top-level should _always_ run out of gas for these tests
1245
1520
  // Check top-level halting message
1246
1521
  // WARNING: only the C++ simulator (or TsVsCpp) will have haltingMessage
@@ -1248,10 +1523,12 @@ async function testSideEffectOpcodeSpam(tester, config, expectToBeTrue) {
1248
1523
  'out of gas',
1249
1524
  'not enough l2gas'
1250
1525
  ];
1251
- const outerCallMetadata = result.callStackMetadata[0];
1252
- const outerReason = outerCallMetadata.haltingMessage?.toLowerCase();
1253
- // expect the reason to match ONE of the allowed reasons
1254
- expectToBeTrue(allowedOuterReasons.some((allowedReason)=>outerReason.includes(allowedReason)));
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
+ }
1255
1532
  return result;
1256
1533
  }
1257
1534
  export async function testOpcodeSpamCase(tester, config, expectToBeTrue = ()=>{}) {