@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.
@@ -159,23 +159,14 @@ import {
159
159
  PUBLIC_LOG_HEADER_LENGTH,
160
160
  } from '@aztec/constants';
161
161
  import { Grumpkin } from '@aztec/foundation/crypto/grumpkin';
162
+ import { randomBigInt } from '@aztec/foundation/crypto/random';
162
163
  import { Fr } from '@aztec/foundation/curves/bn254';
163
164
  import type { Bufferable } from '@aztec/foundation/serialize';
164
165
  import type { CallStackMetadata, PublicTxResult } from '@aztec/stdlib/avm';
165
166
 
166
167
  import assert from 'assert';
167
168
 
168
- import {
169
- Field,
170
- INTEGRAL_TAGS,
171
- type MemoryValue,
172
- TaggedMemory,
173
- TypeTag,
174
- Uint1,
175
- Uint32,
176
- Uint64,
177
- VALID_TAGS,
178
- } from '../avm/avm_memory_types.js';
169
+ import { Field, type MemoryValue, TaggedMemory, TypeTag, Uint1, Uint32 } from '../avm/avm_memory_types.js';
179
170
  import {
180
171
  Add,
181
172
  And,
@@ -192,6 +183,8 @@ import {
192
183
  FieldDiv,
193
184
  GetContractInstance,
194
185
  GetEnvVar,
186
+ InternalCall,
187
+ InternalReturn,
195
188
  Jump,
196
189
  JumpI,
197
190
  KeccakF1600,
@@ -205,6 +198,7 @@ import {
205
198
  NullifierExists,
206
199
  Or,
207
200
  Poseidon2,
201
+ Return,
208
202
  ReturndataCopy,
209
203
  ReturndataSize,
210
204
  Revert,
@@ -215,6 +209,7 @@ import {
215
209
  Sha256Compression,
216
210
  Shl,
217
211
  Shr,
212
+ StaticCall,
218
213
  Sub,
219
214
  SuccessCopy,
220
215
  ToRadixBE,
@@ -223,7 +218,6 @@ import {
223
218
  import { encodeToBytecode } from '../avm/serialization/bytecode_serialization.js';
224
219
  import { Opcode } from '../avm/serialization/instruction_serialization.js';
225
220
  import { deployCustomBytecode, executeCustomBytecode } from './custom_bytecode_tester.js';
226
- import { deployAndExecuteCustomBytecode } from './index.js';
227
221
  import type { PublicTxSimulationTester } from './public_tx_simulation_tester.js';
228
222
 
229
223
  // ============================================================================
@@ -269,6 +263,9 @@ export interface SpamConfig {
269
263
 
270
264
  /** Optional label for this config variant (e.g., UINT8 or MAXSIZE) */
271
265
  label?: string;
266
+
267
+ /** Whether to pass the contract address as calldata[0] */
268
+ addressAsCalldata?: boolean;
272
269
  }
273
270
 
274
271
  /**
@@ -302,19 +299,147 @@ const BYTES_PER_FIELD = Fr.SIZE_IN_BYTES - 1; // 31 bytes of data per field
302
299
  const MAX_BYTECODE_BYTES = (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * BYTES_PER_FIELD;
303
300
 
304
301
  const JUMP_SIZE = encodeToBytecode([new Jump(0)]).length; // JUMP_32
302
+ const INTERNALCALL_SIZE = encodeToBytecode([new InternalCall(0)]).length;
305
303
 
306
304
  // ============================================================================
307
305
  // Type Variant Helpers (for generating multiple configs per opcode)
308
306
  // ============================================================================
309
307
 
310
- const ALL_TAGS = Array.from(VALID_TAGS);
311
- const INT_TAGS = Array.from(INTEGRAL_TAGS);
308
+ // Not using these sets directly because we want to control order
309
+ //const ALL_TAGS = Array.from(VALID_TAGS);
310
+ //const INT_TAGS = Array.from(INTEGRAL_TAGS);
311
+ // Ordered so that limiting #configs per opcode still tests max size (Field)
312
+ const ALL_TAGS = [
313
+ TypeTag.FIELD,
314
+ TypeTag.UINT1,
315
+ TypeTag.UINT8,
316
+ TypeTag.UINT16,
317
+ TypeTag.UINT32,
318
+ TypeTag.UINT64,
319
+ TypeTag.UINT128,
320
+ ];
321
+
322
+ // ordered so that limiting #configs per opcode still tests max size
323
+ const INT_TAGS = [TypeTag.UINT128, TypeTag.UINT1, TypeTag.UINT8, TypeTag.UINT16, TypeTag.UINT32, TypeTag.UINT64];
312
324
 
313
325
  /** Build from tag truncating - shorter name */
314
326
  function withTag(v: bigint, tag: TypeTag): MemoryValue {
315
327
  return TaggedMemory.buildFromTagTruncating(v, tag);
316
328
  }
317
329
 
330
+ // ============================================================================
331
+ // Random Value Helpers (seeded via SEED env var for reproducibility)
332
+ // ============================================================================
333
+
334
+ /** Modulus (really just max+1) for each integer type tag */
335
+ const TAG_MODULI: Partial<Record<TypeTag, bigint>> = {
336
+ [TypeTag.UINT1]: 2n,
337
+ [TypeTag.UINT8]: 256n,
338
+ [TypeTag.UINT16]: 65536n,
339
+ [TypeTag.UINT32]: 0x1_0000_0000n,
340
+ [TypeTag.UINT64]: 0x1_0000_0000_0000_0000n,
341
+ [TypeTag.UINT128]: 0x1_0000_0000_0000_0000_0000_0000_0000_0000n,
342
+ [TypeTag.FIELD]: Fr.MODULUS,
343
+ };
344
+
345
+ /** Generate a random value with the given type tag. Uses SEED env var if set. */
346
+ function randomWithTag(tag: TypeTag): MemoryValue {
347
+ const modulus = TAG_MODULI[tag];
348
+ if (modulus === undefined) {
349
+ throw new Error(`Unsupported tag for random generation: ${TypeTag[tag]}`);
350
+ }
351
+ const value = randomBigInt(modulus);
352
+ return TaggedMemory.buildFromTagTruncating(value, tag);
353
+ }
354
+
355
+ /** Generate a random non-zero value with the given type tag (for division). */
356
+ function randomNonZeroWithTag(tag: TypeTag): MemoryValue {
357
+ const modulus = TAG_MODULI[tag];
358
+ if (modulus === undefined) {
359
+ throw new Error(`Unsupported tag for random generation: ${TypeTag[tag]}`);
360
+ }
361
+ // Generate random in range [1, max) by generating [0, max-1) and adding 1
362
+ const value = randomBigInt(modulus - 1n) + 1n;
363
+ return TaggedMemory.buildFromTagTruncating(value, tag);
364
+ }
365
+
366
+ /** Generate a random non-zero Field value (for field division). */
367
+ function randomNonZeroField(): Field {
368
+ return new Field(randomBigInt(Fr.MODULUS - 1n) + 1n);
369
+ }
370
+
371
+ /** Reserved memory offsets for external call loop (used by CALL spam and side-effect opcodes) */
372
+ const CONST_0_OFFSET = 0; // Uint32(0)
373
+ const CONST_1_OFFSET = 1; // Uint32(1)
374
+ const CONST_MAX_U32_OFFSET = 2; // Uint32(MAX_U32)
375
+ const CALL_ADDR_OFFSET = 3; // copy addr from calldata to here, and then use this addr for CALL
376
+ const CALL_ARGS_OFFSET = CALL_ADDR_OFFSET; // address is the arg to send to CALL
377
+ const CALL_COPY_SIZE_OFFSET = CONST_1_OFFSET; // copy size = 1 (forward calldata[0])
378
+ const CALL_CALLDATA_INDEX_OFFSET = CONST_0_OFFSET; // calldata[0]
379
+ const CALL_L2_GAS_OFFSET = CONST_MAX_U32_OFFSET; // MAX_U32 gets capped to remaining gas by AVM
380
+ const CALL_DA_GAS_OFFSET = CONST_MAX_U32_OFFSET; // MAX_U32 gets capped to remaining gas by AVM
381
+ const CALL_ARGS_SIZE_OFFSET = CONST_1_OFFSET; // argsSize = 1 (forward calldata[0] - might contain contract address)
382
+ const MAX_U32 = 0xffffffffn;
383
+
384
+ /**
385
+ * A SpamConfig for to make external CALLs to an address specified in calldata[0].
386
+ */
387
+ const EXTERNAL_CALL_CONFIG: SpamConfig = {
388
+ setup: [
389
+ // calldata will contain 1 item: the external call address
390
+ { offset: CONST_0_OFFSET, value: new Uint32(0) }, // used for cdStartOffset
391
+ { offset: CONST_1_OFFSET, value: new Uint32(1) }, // used for copySize and argsSize
392
+ { offset: CONST_MAX_U32_OFFSET, value: new Uint32(MAX_U32) }, // l2Gas/daGas - MAX_U32 gets capped to remaining gas
393
+ () => [
394
+ new CalldataCopy(
395
+ /*indirect=*/ 0,
396
+ /*copySizeOffset=*/ CALL_COPY_SIZE_OFFSET,
397
+ /*cdStartOffset=*/ CALL_CALLDATA_INDEX_OFFSET,
398
+ /*dstOffset=*/ CALL_ADDR_OFFSET,
399
+ ),
400
+ ], // address = calldata[0] of parent call
401
+ ],
402
+ targetInstructions: () => [
403
+ new Call(
404
+ /*indirect=*/ 0,
405
+ /*l2GasOffset=*/ CALL_L2_GAS_OFFSET,
406
+ /*daGasOffset=*/ CALL_DA_GAS_OFFSET,
407
+ /*addrOffset=*/ CALL_ADDR_OFFSET,
408
+ /*argsSizeOffset=*/ CALL_ARGS_SIZE_OFFSET,
409
+ /*argsOffset=*/ CALL_ARGS_OFFSET,
410
+ ),
411
+ ],
412
+ addressAsCalldata: true, // indicates that the contract address should be passed as calldata[0]
413
+ };
414
+
415
+ const STATIC_CALL_CONFIG: SpamConfig = {
416
+ setup: [
417
+ // calldata will contain 1 item: the external call address
418
+ { offset: CONST_0_OFFSET, value: new Uint32(0) }, // used for cdStartOffset
419
+ { offset: CONST_1_OFFSET, value: new Uint32(1) }, // used for copySize and argsSize
420
+ { offset: CONST_MAX_U32_OFFSET, value: new Uint32(MAX_U32) }, // l2Gas/daGas - MAX_U32 gets capped to remaining gas
421
+ () => [
422
+ new CalldataCopy(
423
+ /*indirect=*/ 0,
424
+ /*copySizeOffset=*/ CALL_COPY_SIZE_OFFSET,
425
+ /*cdStartOffset=*/ CALL_CALLDATA_INDEX_OFFSET,
426
+ /*dstOffset=*/ CALL_ADDR_OFFSET,
427
+ ),
428
+ ], // address = calldata[0] of parent call
429
+ ],
430
+ targetInstructions: () => [
431
+ new StaticCall(
432
+ /*indirect=*/ 0,
433
+ /*l2GasOffset=*/ CALL_L2_GAS_OFFSET,
434
+ /*daGasOffset=*/ CALL_DA_GAS_OFFSET,
435
+ /*addrOffset=*/ CALL_ADDR_OFFSET,
436
+ /*argsSizeOffset=*/ CALL_ARGS_SIZE_OFFSET,
437
+ /*argsOffset=*/ CALL_ARGS_OFFSET,
438
+ ),
439
+ ],
440
+ addressAsCalldata: true, // indicates that the contract address should be passed as calldata[0]
441
+ };
442
+
318
443
  // ============================================================================
319
444
  // Configuration Map
320
445
  // ============================================================================
@@ -326,13 +451,13 @@ function withTag(v: bigint, tag: TypeTag): MemoryValue {
326
451
  */
327
452
  export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
328
453
  // ═══════════════════════════════════════════════════════════════════════════
329
- // ARITHMETIC - Test with all type variants
454
+ // ARITHMETIC - Test with all type variants (random values, seeded via SEED env var)
330
455
  // ═══════════════════════════════════════════════════════════════════════════
331
456
  [Opcode.ADD_8]: ALL_TAGS.map(tag => ({
332
457
  label: TypeTag[tag],
333
458
  setup: [
334
- { offset: 0, value: withTag(1n, tag) }, // accumulator
335
- { offset: 1, value: withTag(1n, tag) }, // constant addend
459
+ { offset: 0, value: randomWithTag(tag) }, // random accumulator
460
+ { offset: 1, value: randomWithTag(tag) }, // random addend
336
461
  ],
337
462
  targetInstructions: () => [
338
463
  new Add(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.ADD_8, Add.wireFormat8),
@@ -342,8 +467,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
342
467
  [Opcode.SUB_8]: ALL_TAGS.map(tag => ({
343
468
  label: TypeTag[tag],
344
469
  setup: [
345
- { offset: 0, value: withTag(1000000n, tag) }, // start high
346
- { offset: 1, value: withTag(1n, tag) }, // subtract 1
470
+ { offset: 0, value: randomWithTag(tag) }, // random minuend
471
+ { offset: 1, value: randomWithTag(tag) }, // random subtrahend
347
472
  ],
348
473
  targetInstructions: () => [
349
474
  new Sub(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.SUB_8, Sub.wireFormat8),
@@ -353,8 +478,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
353
478
  [Opcode.MUL_8]: ALL_TAGS.map(tag => ({
354
479
  label: TypeTag[tag],
355
480
  setup: [
356
- { offset: 0, value: withTag(2n, tag) }, // accumulator
357
- { offset: 1, value: withTag(2n, tag) }, // multiply by 2
481
+ { offset: 0, value: randomWithTag(tag) }, // random multiplicand
482
+ { offset: 1, value: randomWithTag(tag) }, // random multiplier
358
483
  ],
359
484
  targetInstructions: () => [
360
485
  new Mul(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.MUL_8, Mul.wireFormat8),
@@ -365,8 +490,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
365
490
  [Opcode.DIV_8]: INT_TAGS.map(tag => ({
366
491
  label: TypeTag[tag],
367
492
  setup: [
368
- { offset: 0, value: withTag(111111111111111111n, tag) },
369
- { offset: 1, value: withTag(1n, tag) }, // divide by 2 (identity)
493
+ { offset: 0, value: randomWithTag(tag) }, // random dividend
494
+ { offset: 1, value: randomNonZeroWithTag(tag) }, // random non-zero divisor
370
495
  ],
371
496
  targetInstructions: () => [
372
497
  new Div(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.DIV_8, Div.wireFormat8),
@@ -377,8 +502,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
377
502
  [Opcode.FDIV_8]: [
378
503
  {
379
504
  setup: [
380
- { offset: 0, value: new Field(1000000n) },
381
- { offset: 1, value: new Field(1n) }, // divide by 1 (identity)
505
+ { offset: 0, value: new Field(Fr.random()) }, // random dividend
506
+ { offset: 1, value: randomNonZeroField() }, // random non-zero divisor
382
507
  ],
383
508
  targetInstructions: () => [
384
509
  new FieldDiv(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(
@@ -390,13 +515,13 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
390
515
  ],
391
516
 
392
517
  // ═══════════════════════════════════════════════════════════════════════════
393
- // COMPARATORS - Test with all type variants
518
+ // COMPARATORS - Test with all type variants (random values)
394
519
  // ═══════════════════════════════════════════════════════════════════════════
395
520
  [Opcode.EQ_8]: ALL_TAGS.map(tag => ({
396
521
  label: TypeTag[tag],
397
522
  setup: [
398
- { offset: 0, value: withTag(42n, tag) },
399
- { offset: 1, value: withTag(42n, tag) },
523
+ { offset: 0, value: randomWithTag(tag) }, // random value a
524
+ { offset: 1, value: randomWithTag(tag) }, // random value b
400
525
  ],
401
526
  targetInstructions: () => [
402
527
  new Eq(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2).as(Opcode.EQ_8, Eq.wireFormat8),
@@ -406,8 +531,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
406
531
  [Opcode.LT_8]: ALL_TAGS.map(tag => ({
407
532
  label: TypeTag[tag],
408
533
  setup: [
409
- { offset: 0, value: withTag(1n, tag) },
410
- { offset: 1, value: withTag(1000000n, tag) },
534
+ { offset: 0, value: randomWithTag(tag) }, // random value a
535
+ { offset: 1, value: randomWithTag(tag) }, // random value b
411
536
  ],
412
537
  targetInstructions: () => [
413
538
  new Lt(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2).as(Opcode.LT_8, Lt.wireFormat8),
@@ -417,8 +542,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
417
542
  [Opcode.LTE_8]: ALL_TAGS.map(tag => ({
418
543
  label: TypeTag[tag],
419
544
  setup: [
420
- { offset: 0, value: withTag(1n, tag) },
421
- { offset: 1, value: withTag(1000000n, tag) },
545
+ { offset: 0, value: randomWithTag(tag) }, // random value a
546
+ { offset: 1, value: randomWithTag(tag) }, // random value b
422
547
  ],
423
548
  targetInstructions: () => [
424
549
  new Lte(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2).as(Opcode.LTE_8, Lte.wireFormat8),
@@ -426,13 +551,13 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
426
551
  })),
427
552
 
428
553
  // ═══════════════════════════════════════════════════════════════════════════
429
- // BITWISE - Integer types only (no FIELD)
554
+ // BITWISE - Integer types only (no FIELD) (random values)
430
555
  // ═══════════════════════════════════════════════════════════════════════════
431
556
  [Opcode.AND_8]: INT_TAGS.map(tag => ({
432
557
  label: TypeTag[tag],
433
558
  setup: [
434
- { offset: 0, value: withTag(0xffffffffffffffffffffffffffffffffn, tag) },
435
- { offset: 1, value: withTag(0xffffffffffffffffffffffffffffffffn, tag) },
559
+ { offset: 0, value: randomWithTag(tag) }, // random value a
560
+ { offset: 1, value: randomWithTag(tag) }, // random value b
436
561
  ],
437
562
  targetInstructions: () => [
438
563
  new And(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.AND_8, And.wireFormat8),
@@ -442,8 +567,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
442
567
  [Opcode.OR_8]: INT_TAGS.map(tag => ({
443
568
  label: TypeTag[tag],
444
569
  setup: [
445
- { offset: 0, value: withTag(0xffffffffffffffffffffffffffffffffn, tag) },
446
- { offset: 1, value: withTag(0n, tag) },
570
+ { offset: 0, value: randomWithTag(tag) }, // random value a
571
+ { offset: 1, value: randomWithTag(tag) }, // random value b
447
572
  ],
448
573
  targetInstructions: () => [
449
574
  new Or(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.OR_8, Or.wireFormat8),
@@ -453,8 +578,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
453
578
  [Opcode.XOR_8]: INT_TAGS.map(tag => ({
454
579
  label: TypeTag[tag],
455
580
  setup: [
456
- { offset: 0, value: withTag(0xdeadbeefcafebaben, tag) },
457
- { offset: 1, value: withTag(0x1234567890abcdefn, tag) },
581
+ { offset: 0, value: randomWithTag(tag) }, // random value a
582
+ { offset: 1, value: randomWithTag(tag) }, // random value b
458
583
  ],
459
584
  targetInstructions: () => [
460
585
  new Xor(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.XOR_8, Xor.wireFormat8),
@@ -463,7 +588,7 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
463
588
 
464
589
  [Opcode.NOT_8]: INT_TAGS.map(tag => ({
465
590
  label: TypeTag[tag],
466
- setup: [{ offset: 0, value: withTag(0xffffffffffffffffn, tag) }],
591
+ setup: [{ offset: 0, value: randomWithTag(tag) }], // random value
467
592
  targetInstructions: () => [
468
593
  new Not(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 0).as(Opcode.NOT_8, Not.wireFormat8),
469
594
  ],
@@ -472,8 +597,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
472
597
  [Opcode.SHL_8]: INT_TAGS.map(tag => ({
473
598
  label: TypeTag[tag],
474
599
  setup: [
475
- { offset: 0, value: withTag(1n, tag) },
476
- { offset: 1, value: withTag(1n, tag) },
600
+ { offset: 0, value: randomWithTag(tag) }, // random value to shift
601
+ { offset: 1, value: withTag(1n, tag) }, // shift by 1 (small fixed amount to avoid overflow)
477
602
  ],
478
603
  targetInstructions: () => [
479
604
  new Shl(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.SHL_8, Shl.wireFormat8),
@@ -483,8 +608,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
483
608
  [Opcode.SHR_8]: INT_TAGS.map(tag => ({
484
609
  label: TypeTag[tag],
485
610
  setup: [
486
- { offset: 0, value: withTag(0xffffffffffffffffn, tag) },
487
- { offset: 1, value: withTag(1n, tag) },
611
+ { offset: 0, value: randomWithTag(tag) }, // random value to shift
612
+ { offset: 1, value: withTag(1n, tag) }, // shift by 1 (small fixed amount)
488
613
  ],
489
614
  targetInstructions: () => [
490
615
  new Shr(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 0).as(Opcode.SHR_8, Shr.wireFormat8),
@@ -492,11 +617,11 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
492
617
  })),
493
618
 
494
619
  // ═══════════════════════════════════════════════════════════════════════════
495
- // CAST / MOV - Test with all type variants
620
+ // CAST / MOV - Test with all type variants (random values)
496
621
  // ═══════════════════════════════════════════════════════════════════════════
497
622
  [Opcode.CAST_8]: ALL_TAGS.map(tag => ({
498
623
  label: TypeTag[tag],
499
- setup: [{ offset: 0, value: withTag(42n, tag) }],
624
+ setup: [{ offset: 0, value: randomWithTag(tag) }], // random value to cast
500
625
  targetInstructions: () => [
501
626
  new Cast(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 1, /*dstTag=*/ TypeTag.UINT32).as(
502
627
  Opcode.CAST_8,
@@ -507,7 +632,7 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
507
632
 
508
633
  [Opcode.MOV_8]: ALL_TAGS.map(tag => ({
509
634
  label: TypeTag[tag],
510
- setup: [{ offset: 0, value: withTag(42n, tag) }],
635
+ setup: [{ offset: 0, value: randomWithTag(tag) }], // random value to move
511
636
  targetInstructions: () => [
512
637
  new Mov(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 1).as(Opcode.MOV_8, Mov.wireFormat8),
513
638
  ],
@@ -583,6 +708,65 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
583
708
  },
584
709
  ],
585
710
 
711
+ // INTERNALCALL: calls itself infinitely by jumping to its own PC (PC 0, since no setup)
712
+ // Creates infinite recursion until OOG (internal call stack grows forever)
713
+ [Opcode.INTERNALCALL]: [
714
+ {
715
+ setup: [],
716
+ targetInstructions: () => [new InternalCall(/*loc=*/ 0)],
717
+ },
718
+ ],
719
+
720
+ // INTERNALRETURN: needs INTERNALCALL to return without error
721
+ // Layout: INTERNALCALL(10) -> JUMP(0) -> INTERNALRETURN
722
+ // INTERNALCALL jumps to INTERNALRETURN, which returns to JUMP, which loops back
723
+ [Opcode.INTERNALRETURN]: [
724
+ {
725
+ setup: [],
726
+ targetInstructions: () => [
727
+ new InternalCall(/*loc=*/ INTERNALCALL_SIZE + JUMP_SIZE), // jump to INTERNALRETURN
728
+ new Jump(/*jumpOffset=*/ 0), // loop back to start
729
+ new InternalReturn(), // return back to jump
730
+ ],
731
+ },
732
+ ],
733
+
734
+ // CALL (EXTERNALCALL): calls the current contract address (self) in a loop
735
+ // Contract address is passed via calldata[0] and propagated to nested calls
736
+ [Opcode.CALL]: [EXTERNAL_CALL_CONFIG],
737
+ [Opcode.STATICCALL]: [STATIC_CALL_CONFIG],
738
+
739
+ // RETURN: terminates execution, so we need to use the two-contract pattern
740
+ // Outer contract CALLs inner contract in a loop, inner contract does RETURN
741
+ [Opcode.RETURN]: [
742
+ {
743
+ setup: [
744
+ { offset: 0, value: new Uint32(0) }, // returnSize = 0
745
+ ],
746
+ targetInstructions: () => [
747
+ new Return(/*indirect=*/ 0, /*returnSizeOffset=*/ 0, /*returnOffset=*/ 0), // return nothing (size=0)
748
+ ],
749
+ // Use the side-effect-limit pattern (even though it's not a side-effect) as it fits
750
+ // this case (we want to CALL, RETURN, then CALL again back in parent). We omit "cleanup"
751
+ // because we don't need to REVERT as we do for real side-effects.
752
+ limit: 1, // RETURN can only execute once per call
753
+ },
754
+ ],
755
+
756
+ // REVERT: terminates execution, so we need to use the two-contract pattern
757
+ // Outer contract CALLs inner contract in a loop, inner contract does REVERT
758
+ [Opcode.REVERT_8]: [
759
+ {
760
+ setup: [
761
+ { offset: 0, value: new Uint32(0) }, // retSize = 0
762
+ ],
763
+ targetInstructions: () => [
764
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 0, /*returnOffset=*/ 1).as(Opcode.REVERT_8, Revert.wireFormat8),
765
+ ],
766
+ limit: 1, // REVERT can only execute once per call
767
+ },
768
+ ],
769
+
586
770
  // ═══════════════════════════════════════════════════════════════════════════
587
771
  // ENVIRONMENT
588
772
  // ═══════════════════════════════════════════════════════════════════════════
@@ -598,14 +782,38 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
598
782
  },
599
783
  ],
600
784
 
785
+ // CALLDATACOPY has dynamic gas scaling with copySize
601
786
  [Opcode.CALLDATACOPY]: [
602
787
  {
603
- // CalldataCopy(indirect=0, copySizeOffset=0, cdStartOffset=1, dstOffset=2)
604
- // Copies M[0]=1 elements starting from CD[M[1]]=CD[0] into M[2].
605
- // In other words: M[2] = CD[0]
788
+ label: 'Min copy size',
789
+ // CalldataCopy with copySize=0 is a no-op but still executes the opcode
790
+ setup: [
791
+ { offset: 0, value: new Uint32(0n) }, // copySize = 0 (minimum)
792
+ { offset: 1, value: new Uint32(0n) }, // cdStart = 0
793
+ ],
794
+ targetInstructions: () => [
795
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*cdStartOffset=*/ 1, /*dstOffset=*/ 2),
796
+ ],
797
+ },
798
+ {
799
+ label: 'Large copy size',
800
+ // Large copySize with large dynamic gas - will OOG quickly
801
+ // NOTE: we don't want it so large that it exceeds memory bounds (MAX_MEMORY_SIZE = 2^32)
802
+ // and we really want it small enough that we run at least 1 successful target opcode.
803
+ setup: [
804
+ { offset: 0, value: new Uint32(1000n) }, // copySize = 1000 (large enough to show scaling)
805
+ { offset: 1, value: new Uint32(0n) }, // cdStart = 0
806
+ ],
807
+ targetInstructions: () => [
808
+ new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*cdStartOffset=*/ 1, /*dstOffset=*/ 2),
809
+ ],
810
+ },
811
+ {
812
+ label: 'Near min copy size of 1',
813
+ // Near-min but actually copies data (more meaningful than size=0 no-op)
606
814
  setup: [
607
815
  { offset: 0, value: new Uint32(1n) }, // copySize = 1
608
- { offset: 1, value: new Uint32(0n) }, // cdStart = 0 (start copying from calldata[0])
816
+ { offset: 1, value: new Uint32(0n) }, // cdStart = 0
609
817
  ],
610
818
  targetInstructions: () => [
611
819
  new CalldataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*cdStartOffset=*/ 1, /*dstOffset=*/ 2),
@@ -627,10 +835,36 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
627
835
  },
628
836
  ],
629
837
 
838
+ // RETURNDATACOPY has dynamic gas scaling with copySize
630
839
  [Opcode.RETURNDATACOPY]: [
631
840
  {
841
+ label: 'Min copy size',
632
842
  setup: [
633
- { offset: 0, value: new Uint32(0n) }, // copySize = 0
843
+ { offset: 0, value: new Uint32(0n) }, // copySize = 0 (minimum)
844
+ { offset: 1, value: new Uint32(0n) }, // rdOffset
845
+ ],
846
+ targetInstructions: () => [
847
+ new ReturndataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*rdStartOffset=*/ 1, /*dstOffset=*/ 2),
848
+ ],
849
+ },
850
+ {
851
+ label: 'Large copy size',
852
+ // Large copySize to maximize dynamic gas - will OOG quickly
853
+ // NOTE: we don't want it so large that it exceeds memory bounds (MAX_MEMORY_SIZE = 2^32)
854
+ // and we really want it small enough that we run at least 1 successful target opcode.
855
+ setup: [
856
+ { offset: 0, value: new Uint32(1000n) }, // copySize = 1000 (large enough to show scaling)
857
+ { offset: 1, value: new Uint32(0n) }, // rdOffset
858
+ ],
859
+ targetInstructions: () => [
860
+ new ReturndataCopy(/*indirect=*/ 0, /*copySizeOffset=*/ 0, /*rdStartOffset=*/ 1, /*dstOffset=*/ 2),
861
+ ],
862
+ },
863
+ {
864
+ label: 'Near min copy size of 1',
865
+ // Near-min but actually copies data (more meaningful than size=0 no-op)
866
+ setup: [
867
+ { offset: 0, value: new Uint32(1n) }, // copySize = 1
634
868
  { offset: 1, value: new Uint32(0n) }, // rdOffset
635
869
  ],
636
870
  targetInstructions: () => [
@@ -644,16 +878,38 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
644
878
  // ═══════════════════════════════════════════════════════════════════════════
645
879
  [Opcode.SLOAD]: [
646
880
  {
647
- setup: [{ offset: 0, value: new Field(0n) }], // slot
881
+ label: 'Cold read (slot not written)',
882
+ setup: [{ offset: 0, value: new Field(Fr.random()) }], // random slot
648
883
  targetInstructions: () => [new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*dstOffset=*/ 1)],
649
884
  },
885
+ {
886
+ label: 'Warm read (SSTORE first)',
887
+ // Memory layout: slot (incremented), value, constant 1, revertSize, loaded value
888
+ setup: [
889
+ { offset: 0, value: new Field(Fr.random()) }, // slot (will be incremented)
890
+ { offset: 1, value: new Field(Fr.random()) }, // value to store
891
+ { offset: 2, value: new Field(1n) }, // constant 1 for ADD
892
+ { offset: 3, value: new Uint32(0n) }, // revertSize
893
+ ],
894
+ targetInstructions: () => [
895
+ new SStore(/*indirect=*/ 0, /*srcOffset=*/ 1, /*slotOffset=*/ 0),
896
+ new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*dstOffset=*/ 4),
897
+ new Add(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 0).as(Opcode.ADD_8, Add.wireFormat8), // slot++
898
+ ],
899
+ cleanupInstructions: () => [
900
+ new Revert(/*indirect=*/ 0, /*retSizeOffset=*/ 3, /*returnOffset=*/ 0).as(Opcode.REVERT_8, Revert.wireFormat8),
901
+ ],
902
+ limit: MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
903
+ },
650
904
  ],
651
905
 
652
906
  [Opcode.NOTEHASHEXISTS]: [
653
907
  {
908
+ // Note: Can't easily do "write first" version - would need to know the leaf index
909
+ // that EMITNOTEHASH will produce, which depends on tree state
654
910
  setup: [
655
- { offset: 0, value: new Field(0n) }, // noteHash
656
- { offset: 1, value: new Uint64(0n) }, // leafIndex
911
+ { offset: 0, value: new Field(Fr.random()) }, // random noteHash
912
+ { offset: 1, value: randomWithTag(TypeTag.UINT64) }, // random leafIndex
657
913
  ],
658
914
  targetInstructions: () => [
659
915
  new NoteHashExists(/*indirect=*/ 0, /*noteHashOffset=*/ 0, /*leafIndexOffset=*/ 1, /*existsOffset=*/ 2),
@@ -663,21 +919,47 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
663
919
 
664
920
  [Opcode.NULLIFIEREXISTS]: [
665
921
  {
922
+ label: 'Non-existent nullifier',
666
923
  setup: [
667
- { offset: 0, value: new Field(0n) }, // nullifier
668
- { offset: 1, value: new Field(0n) }, // address
924
+ { offset: 0, value: new Field(Fr.random()) }, // random nullifier
925
+ { offset: 1, value: new Field(Fr.random()) }, // random address
669
926
  ],
670
927
  targetInstructions: () => [
671
928
  new NullifierExists(/*indirect=*/ 0, /*nullifierOffset=*/ 0, /*addressOffset=*/ 1, /*existsOffset=*/ 2),
672
929
  ],
673
930
  },
931
+ {
932
+ label: 'Existing nullifier (EMITNULLIFIER first)',
933
+ // Memory layout: nullifier (incremented), constant 1, current address (from GETENVVAR), revertSize, exists result
934
+ setup: [
935
+ { offset: 0, value: new Field(Fr.random()) }, // nullifier (will be incremented)
936
+ { offset: 1, value: new Field(1n) }, // constant 1 for ADD
937
+ () => [
938
+ // Get current contract address into offset 2
939
+ new GetEnvVar(/*indirect=*/ 0, /*dstOffset=*/ 2, /*varEnum=*/ 0).as(
940
+ Opcode.GETENVVAR_16,
941
+ GetEnvVar.wireFormat16,
942
+ ),
943
+ ],
944
+ { offset: 3, value: new Uint32(0n) }, // revertSize
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), // nullifier++
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
+ },
674
956
  ],
675
957
 
676
958
  [Opcode.L1TOL2MSGEXISTS]: [
677
959
  {
678
960
  setup: [
679
- { offset: 0, value: new Field(0n) }, // msgHash
680
- { offset: 1, value: new Uint64(0n) }, // msgLeafIndex
961
+ { offset: 0, value: new Field(Fr.random()) }, // random msgHash
962
+ { offset: 1, value: randomWithTag(TypeTag.UINT64) }, // random msgLeafIndex
681
963
  ],
682
964
  targetInstructions: () => [
683
965
  new L1ToL2MessageExists(/*indirect=*/ 0, /*msgHashOffset=*/ 0, /*msgLeafIndexOffset=*/ 1, /*existsOffset=*/ 2),
@@ -687,7 +969,16 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
687
969
 
688
970
  [Opcode.GETCONTRACTINSTANCE]: [
689
971
  {
690
- setup: [{ offset: 0, value: new Field(0n) }], // address
972
+ // Use GETENVVAR to get current contract address (varEnum 0 = ADDRESS)
973
+ // This ensures we're querying a valid deployed contract
974
+ setup: [
975
+ () => [
976
+ new GetEnvVar(/*indirect=*/ 0, /*dstOffset=*/ 0, /*varEnum=*/ 0).as(
977
+ Opcode.GETENVVAR_16,
978
+ GetEnvVar.wireFormat16,
979
+ ),
980
+ ],
981
+ ],
691
982
  // memberEnum 0 = DEPLOYER
692
983
  targetInstructions: () => [
693
984
  new GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1, /*memberEnum=*/ 0),
@@ -701,7 +992,7 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
701
992
  [Opcode.EMITNOTEHASH]: [
702
993
  {
703
994
  setup: [
704
- { offset: 0, value: new Field(0x1000n) },
995
+ { offset: 0, value: new Field(Fr.random()) }, // random noteHash
705
996
  { offset: 1, value: new Uint32(0n) }, // revertSize
706
997
  ],
707
998
  targetInstructions: () => [new EmitNoteHash(/*indirect=*/ 0, /*noteHashOffset=*/ 0)],
@@ -717,7 +1008,7 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
717
1008
  // Nullifiers must be unique - increment value after each emit
718
1009
  // Memory layout: offset 0 = nullifier value, offset 1 = constant 1 for incrementing
719
1010
  setup: [
720
- { offset: 0, value: new Field(0x2000n) }, // nullifier (will be incremented)
1011
+ { offset: 0, value: new Field(Fr.random()) }, // random nullifier (will be incremented)
721
1012
  { offset: 1, value: new Field(1n) }, // constant 1 for ADD
722
1013
  { offset: 2, value: new Uint32(0n) }, // revertSize
723
1014
  ],
@@ -735,8 +1026,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
735
1026
  [Opcode.SENDL2TOL1MSG]: [
736
1027
  {
737
1028
  setup: [
738
- { offset: 0, value: new Field(1n) }, // recipient
739
- { offset: 1, value: new Field(0x3000n) }, // content
1029
+ { offset: 0, value: new Field(Fr.random()) }, // random recipient
1030
+ { offset: 1, value: new Field(Fr.random()) }, // random content
740
1031
  { offset: 2, value: new Uint32(0n) }, // revertSize
741
1032
  ],
742
1033
  targetInstructions: () => [new SendL2ToL1Message(/*indirect=*/ 0, /*recipientOffset=*/ 0, /*contentOffset=*/ 1)],
@@ -754,8 +1045,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
754
1045
  {
755
1046
  label: 'Same slot (no limit)',
756
1047
  setup: [
757
- { offset: 0, value: new Field(42n) }, // value
758
- { offset: 1, value: new Field(0x100n) }, // slot (same slot each iteration)
1048
+ { offset: 0, value: new Field(Fr.random()) }, // random value
1049
+ { offset: 1, value: new Field(Fr.random()) }, // random slot (same slot each iteration)
759
1050
  { offset: 2, value: new Uint32(0n) }, // revertSize
760
1051
  ],
761
1052
  targetInstructions: () => [new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*slotOffset=*/ 1)],
@@ -766,8 +1057,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
766
1057
  {
767
1058
  label: 'Unique slots (side-effect limited)',
768
1059
  setup: [
769
- { offset: 0, value: new Field(42n) }, // value (constant)
770
- { offset: 1, value: new Field(0x100n) }, // slot (will be incremented)
1060
+ { offset: 0, value: new Field(Fr.random()) }, // random value (constant)
1061
+ { offset: 1, value: new Field(Fr.random()) }, // random slot (will be incremented)
771
1062
  { offset: 2, value: new Field(1n) }, // constant 1 for ADD
772
1063
  { offset: 3, value: new Uint32(0n) }, // revertSize
773
1064
  ],
@@ -820,14 +1111,14 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
820
1111
  ],
821
1112
 
822
1113
  // ═══════════════════════════════════════════════════════════════════════════
823
- // GADGETS - Use non-trivial inputs to avoid special-case optimizations
1114
+ // GADGETS - Random inputs (seeded via SEED env var)
824
1115
  // ═══════════════════════════════════════════════════════════════════════════
825
1116
  [Opcode.POSEIDON2]: [
826
1117
  {
827
1118
  // Poseidon2 takes 4 field elements as input
828
1119
  setup: Array.from({ length: 4 }, (_, i) => ({
829
1120
  offset: i,
830
- value: new Field(BigInt(0xdeadbeef + i * 0x1111)),
1121
+ value: new Field(Fr.random()), // random field element
831
1122
  })),
832
1123
  // Poseidon hash data at M[0..3], write result to M[0:3] (reuse results as next inputs)
833
1124
  targetInstructions: () => [new Poseidon2(/*indirect=*/ 0, /*inputStateOffset=*/ 0, /*outputStateOffset=*/ 0)],
@@ -837,19 +1128,15 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
837
1128
  [Opcode.SHA256COMPRESSION]: [
838
1129
  {
839
1130
  setup: [
840
- // State: 8 x UINT32 at offsets 0-7 (use SHA256 initial hash values)
841
- { offset: 0, value: new Uint32(0x6a09e667n) },
842
- { offset: 1, value: new Uint32(0xbb67ae85n) },
843
- { offset: 2, value: new Uint32(0x3c6ef372n) },
844
- { offset: 3, value: new Uint32(0xa54ff53an) },
845
- { offset: 4, value: new Uint32(0x510e527fn) },
846
- { offset: 5, value: new Uint32(0x9b05688cn) },
847
- { offset: 6, value: new Uint32(0x1f83d9abn) },
848
- { offset: 7, value: new Uint32(0x5be0cd19n) },
849
- // Inputs: 16 x UINT32 at offsets 8-23 (non-trivial message block)
1131
+ // State: 8 x UINT32 at offsets 0-7 (random initial state)
1132
+ ...Array.from({ length: 8 }, (_, i) => ({
1133
+ offset: i,
1134
+ value: randomWithTag(TypeTag.UINT32),
1135
+ })),
1136
+ // Inputs: 16 x UINT32 at offsets 8-23 (random message block)
850
1137
  ...Array.from({ length: 16 }, (_, i) => ({
851
1138
  offset: 8 + i,
852
- value: new Uint32((0xcafebaben + BigInt(i) * 0x01010101n) & 0xffffffffn),
1139
+ value: randomWithTag(TypeTag.UINT32),
853
1140
  })),
854
1141
  ],
855
1142
  targetInstructions: () => [
@@ -860,10 +1147,10 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
860
1147
 
861
1148
  [Opcode.KECCAKF1600]: [
862
1149
  {
863
- // Keccak state: 25 x UINT64 (5x5 lane array)
1150
+ // Keccak state: 25 x UINT64 (5x5 lane array) with random values
864
1151
  setup: Array.from({ length: 25 }, (_, i) => ({
865
1152
  offset: i,
866
- value: new Uint64((0xdeadbeefcafebaben + BigInt(i) * 0x0101010101010101n) & 0xffffffffffffffffn),
1153
+ value: randomWithTag(TypeTag.UINT64),
867
1154
  })),
868
1155
  targetInstructions: () => [new KeccakF1600(/*indirect=*/ 0, /*dstOffset=*/ 0, /*inputOffset=*/ 0)],
869
1156
  },
@@ -895,12 +1182,33 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
895
1182
  },
896
1183
  ],
897
1184
 
1185
+ // TORADIXBE has dynamic gas scaling with numLimbs
898
1186
  [Opcode.TORADIXBE]: [
899
1187
  {
1188
+ label: 'Min limbs',
1189
+ setup: [
1190
+ { offset: 0, value: new Field(1n) }, // small value that fits in 1 limb (can't randomize - would truncate)
1191
+ { offset: 1, value: new Uint32(2n) }, // radix = 2 (binary)
1192
+ { offset: 2, value: new Uint32(1n) }, // numLimbs = 1 (minimum)
1193
+ { offset: 3, value: new Uint1(0n) }, // outputBits = false
1194
+ ],
1195
+ targetInstructions: () => [
1196
+ new ToRadixBE(
1197
+ /*indirect=*/ 0,
1198
+ /*srcOffset=*/ 0,
1199
+ /*radixOffset=*/ 1,
1200
+ /*numLimbsOffset=*/ 2,
1201
+ /*outputBitsOffset=*/ 3,
1202
+ /*dstOffset=*/ 4,
1203
+ ),
1204
+ ],
1205
+ },
1206
+ {
1207
+ label: 'Max limbs',
900
1208
  setup: [
901
- { offset: 0, value: new Field(0xdeadbeefcafebaben) }, // non-trivial src value
902
- { offset: 1, value: new Uint32(16n) }, // radix = 16 (hex)
903
- { offset: 2, value: new Uint32(16n) }, // numLimbs = 16
1209
+ { offset: 0, value: new Field(Fr.random()) }, // random field value (fits in 256 bits)
1210
+ { offset: 1, value: new Uint32(2n) }, // radix = 2 (binary)
1211
+ { offset: 2, value: new Uint32(256n) }, // numLimbs = 256 (max bits in field)
904
1212
  { offset: 3, value: new Uint1(0n) }, // outputBits = false
905
1213
  ],
906
1214
  targetInstructions: () => [
@@ -919,6 +1227,8 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
919
1227
  // ═══════════════════════════════════════════════════════════════════════════
920
1228
  // MISC
921
1229
  // ═══════════════════════════════════════════════════════════════════════════
1230
+ // DEBUGLOG only has base gas (no dynamic gas scaling) - memory reads only happen
1231
+ // when collectDebugLogs config is enabled
922
1232
  [Opcode.DEBUGLOG]: [
923
1233
  {
924
1234
  setup: [
@@ -939,6 +1249,28 @@ export const SPAM_CONFIGS: Partial<Record<Opcode, SpamConfig[]>> = {
939
1249
  ),
940
1250
  ],
941
1251
  },
1252
+ // No real reason to test this by default since debug logs only meaningfully do scaling work
1253
+ // when collectDebugLogs is enabled.
1254
+ // {
1255
+ // label: 'Max sizes',
1256
+ // setup: [
1257
+ // { offset: 0, value: new Field(0n) }, // level (0 = trace)
1258
+ // { offset: 1, value: new Field(0n) }, // message start
1259
+ // { offset: 2, value: new Field(0n) }, // fields start
1260
+ // { offset: 3, value: new Uint32(1000n) }, // fieldsSize = 1000 (large enough to show scaling)
1261
+ // ],
1262
+ // // messageSize = 1000 (large enough to show scaling)
1263
+ // targetInstructions: () => [
1264
+ // new DebugLog(
1265
+ // /*indirect=*/ 0,
1266
+ // /*levelOffset=*/ 0,
1267
+ // /*messageOffset=*/ 1,
1268
+ // /*fieldsOffset=*/ 2,
1269
+ // /*fieldsSizeOffset=*/ 3,
1270
+ // /*messageSize=*/ 1000,
1271
+ // ),
1272
+ // ],
1273
+ // },
942
1274
  ],
943
1275
  };
944
1276
 
@@ -1083,7 +1415,7 @@ export function createOpcodeSpamBytecode(config: SpamConfig): Buffer {
1083
1415
  // 1. Setup memory
1084
1416
  appendSetupInstructions(instructions, config.setup);
1085
1417
 
1086
- // 2. Infinite loop - maximize iterations until out-of-gas
1418
+ // 2. Infinite loop - maximize calls to target until out-of-gas
1087
1419
  appendInfiniteLoop(instructions, config);
1088
1420
 
1089
1421
  return encodeToBytecode(instructions);
@@ -1117,78 +1449,23 @@ export function createSideEffectSpamBytecode(config: SpamConfig): Buffer {
1117
1449
  return encodeToBytecode(instructions);
1118
1450
  }
1119
1451
 
1120
- /** Reserved memory offsets for outer call loop */
1121
- const CONST_1_OFFSET = 0;
1122
- const CALL_L2_GAS_OFFSET = 1;
1123
- const CALL_DA_GAS_OFFSET = 2;
1124
- const CALL_ADDR_OFFSET = 3;
1125
- const CALL_ARGS_SIZE = 4;
1126
- const CALL_ARGS_OFFSET = 5;
1127
- const CALLDATA_INDEX_OFFSET = 6; // calldata index as in calldata[index]
1128
-
1129
- /**
1130
- * A SpamConfig for an external call loop.
1131
- */
1132
- const EXTERNAL_CALL_LOOP_CONFIG: SpamConfig = {
1133
- setup: [
1134
- // calldata will contain 1 item: the external call address
1135
- { offset: CONST_1_OFFSET, value: new Uint32(1) }, // calldata size = 1
1136
- { offset: CALLDATA_INDEX_OFFSET, value: new Uint32(0) }, // we want calldata[0]
1137
- { offset: CALL_L2_GAS_OFFSET, value: new Uint32(0xffffffffn) }, // l2Gas = max uint32
1138
- { offset: CALL_DA_GAS_OFFSET, value: new Uint32(0xffffffffn) }, // daGas = max uint32
1139
- () => [
1140
- new CalldataCopy(
1141
- /*indirect=*/ 0,
1142
- /*copySizeOffset=*/ CONST_1_OFFSET,
1143
- /*cdStartOffset=*/ CALLDATA_INDEX_OFFSET,
1144
- /*dstOffset=*/ CALL_ADDR_OFFSET,
1145
- ),
1146
- ], // address = calldata[0] of parent call
1147
- { offset: CALL_ARGS_SIZE, value: new Uint32(0) }, // argsSize = max uint32
1148
- { offset: CALL_ARGS_SIZE, value: new Uint32(0) }, // argsSize = max uint32
1149
- ],
1150
- targetInstructions: () => [
1151
- new Call(
1152
- /*indirect=*/ 0,
1153
- /*l2GasOffset=*/ CALL_L2_GAS_OFFSET,
1154
- /*daGasOffset=*/ CALL_DA_GAS_OFFSET,
1155
- /*addrOffset=*/ CALL_ADDR_OFFSET,
1156
- /*argsSizeOffset=*/ CALL_ARGS_SIZE,
1157
- /*argsOffset=*/ CALL_ARGS_OFFSET,
1158
- ),
1159
- ],
1160
- };
1161
-
1162
- /**
1163
- * Create bytecode that makes an external call in a loop.
1164
- *
1165
- * @returns the bytecode for the external call loop
1166
- */
1167
- export function createExternalCallLoopBytecode(): Buffer {
1168
- const config = EXTERNAL_CALL_LOOP_CONFIG;
1169
- const instructions: Bufferable[] = [];
1170
-
1171
- // 1. Setup memory
1172
- appendSetupInstructions(instructions, config.setup);
1173
- // 2. Infinite loop of external calls - maximize iterations until out-of-gas
1174
- appendInfiniteLoop(instructions, config);
1175
- return encodeToBytecode(instructions);
1176
- }
1177
-
1178
1452
  async function testStandardOpcodeSpam(
1179
1453
  tester: PublicTxSimulationTester,
1180
1454
  config: SpamConfig,
1181
1455
  expectToBeTrue: (x: boolean) => void,
1182
1456
  ): Promise<PublicTxResult> {
1183
1457
  const bytecode = createOpcodeSpamBytecode(config);
1184
- const result = await deployAndExecuteCustomBytecode(bytecode, tester, config.label);
1458
+ const contract = await deployCustomBytecode(bytecode, tester, config.label);
1459
+ // Should we pass the contract address as calldata?
1460
+ const calldata = config.addressAsCalldata ? [contract.address.toField()] : [];
1461
+ const result = await executeCustomBytecode(contract, tester, config.label, calldata);
1185
1462
 
1186
1463
  // should have halted with out of gas
1187
1464
  expectToBeTrue(!result.revertCode.isOK());
1188
1465
  const revertReason = result.findRevertReason()?.message.toLowerCase();
1189
1466
  const allowedReasons = ['out of gas', 'not enough l2gas'];
1190
1467
  // expect the reason to match ONE of the allowed reasons
1191
- expectToBeTrue(allowedReasons.some(allowedReason => revertReason!.includes(allowedReason)));
1468
+ expectToBeTrue(allowedReasons.some(allowedReason => revertReason?.includes(allowedReason)));
1192
1469
  return result;
1193
1470
  }
1194
1471
 
@@ -1197,8 +1474,10 @@ async function testSideEffectOpcodeSpam(
1197
1474
  config: SpamConfig,
1198
1475
  expectToBeTrue: (x: boolean) => void,
1199
1476
  ): Promise<PublicTxResult> {
1477
+ // Inner contract will spam the side-effect limited opcode up to its limit, then REVERT
1200
1478
  const innerBytecode = createSideEffectSpamBytecode(config);
1201
- const outerBytecode = createExternalCallLoopBytecode();
1479
+ // Outer contract will CALL to inner contract in a loop
1480
+ const outerBytecode = createOpcodeSpamBytecode(EXTERNAL_CALL_CONFIG);
1202
1481
  const innerContract = await deployCustomBytecode(innerBytecode, tester, `${config.label}_Inner`);
1203
1482
  const outerContract = await deployCustomBytecode(outerBytecode, tester, `${config.label}_Outer`);
1204
1483
  // Outer contract reads calldata[0] as inner contract address to CALL to
@@ -1209,16 +1488,18 @@ async function testSideEffectOpcodeSpam(
1209
1488
  const revertReason = result.findRevertReason()?.message.toLowerCase();
1210
1489
  const allowedReasons = ['assertion failed', 'out of gas', 'not enough l2gas'];
1211
1490
  // expect the reason to match ONE of the allowed reasons
1212
- expectToBeTrue(allowedReasons.some(allowedReason => revertReason!.includes(allowedReason)));
1491
+ expectToBeTrue(allowedReasons.some(allowedReason => revertReason?.includes(allowedReason)));
1213
1492
 
1214
1493
  // Top-level should _always_ run out of gas for these tests
1215
1494
  // Check top-level halting message
1216
1495
  // WARNING: only the C++ simulator (or TsVsCpp) will have haltingMessage
1217
1496
  const allowedOuterReasons = ['out of gas', 'not enough l2gas'];
1218
- const outerCallMetadata = result.callStackMetadata[0] as CallStackMetadata;
1219
- const outerReason = outerCallMetadata.haltingMessage?.toLowerCase();
1220
- // expect the reason to match ONE of the allowed reasons
1221
- expectToBeTrue(allowedOuterReasons.some(allowedReason => outerReason!.includes(allowedReason)));
1497
+ if (result.callStackMetadata && result.callStackMetadata.length > 0) {
1498
+ const outerCallMetadata = result.callStackMetadata[0] as CallStackMetadata;
1499
+ const outerReason = outerCallMetadata.haltingMessage?.toLowerCase();
1500
+ // expect the reason to match ONE of the allowed reasons
1501
+ expectToBeTrue(allowedOuterReasons.some(allowedReason => outerReason?.includes(allowedReason)));
1502
+ }
1222
1503
 
1223
1504
  return result;
1224
1505
  }