@aztec/ethereum 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d

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 (144) hide show
  1. package/dest/client.d.ts +19 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +52 -3
  4. package/dest/config.d.ts +15 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +29 -9
  7. package/dest/contracts/chain_state_override.d.ts +78 -0
  8. package/dest/contracts/chain_state_override.d.ts.map +1 -0
  9. package/dest/contracts/chain_state_override.js +137 -0
  10. package/dest/contracts/empire_base.d.ts +1 -3
  11. package/dest/contracts/empire_base.d.ts.map +1 -1
  12. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  13. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  14. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  15. package/dest/contracts/governance.d.ts +135 -35
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +226 -15
  18. package/dest/contracts/governance_proposer.d.ts +33 -4
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +52 -13
  21. package/dest/contracts/gse.d.ts +4 -2
  22. package/dest/contracts/gse.d.ts.map +1 -1
  23. package/dest/contracts/gse.js +2 -2
  24. package/dest/contracts/inbox.d.ts +3 -3
  25. package/dest/contracts/inbox.d.ts.map +1 -1
  26. package/dest/contracts/inbox.js +9 -3
  27. package/dest/contracts/index.d.ts +3 -3
  28. package/dest/contracts/index.d.ts.map +1 -1
  29. package/dest/contracts/index.js +2 -2
  30. package/dest/contracts/multicall.d.ts +79 -13
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +117 -95
  33. package/dest/contracts/outbox.d.ts +27 -4
  34. package/dest/contracts/outbox.d.ts.map +1 -1
  35. package/dest/contracts/outbox.js +34 -7
  36. package/dest/contracts/rollup.d.ts +5401 -34
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +271 -119
  39. package/dest/contracts/slashing_proposer.d.ts +171 -0
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/slashing_proposer.js +753 -0
  42. package/dest/contracts/watch_event.d.ts +36 -0
  43. package/dest/contracts/watch_event.d.ts.map +1 -0
  44. package/dest/contracts/watch_event.js +66 -0
  45. package/dest/deploy_aztec_l1_contracts.d.ts +4 -7
  46. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  47. package/dest/deploy_aztec_l1_contracts.js +15 -8
  48. package/dest/foundry_binary.d.ts +13 -0
  49. package/dest/foundry_binary.d.ts.map +1 -0
  50. package/dest/foundry_binary.js +52 -0
  51. package/dest/generated/l1-contracts-defaults.d.ts +2 -2
  52. package/dest/generated/l1-contracts-defaults.js +2 -2
  53. package/dest/l1_artifacts.d.ts +14941 -16273
  54. package/dest/l1_artifacts.d.ts.map +1 -1
  55. package/dest/l1_artifacts.js +9 -24
  56. package/dest/l1_contract_addresses.d.ts +61 -65
  57. package/dest/l1_contract_addresses.d.ts.map +1 -1
  58. package/dest/l1_contract_addresses.js +101 -82
  59. package/dest/l1_reader.d.ts +3 -5
  60. package/dest/l1_reader.d.ts.map +1 -1
  61. package/dest/l1_reader.js +3 -6
  62. package/dest/l1_tx_utils/config.js +3 -3
  63. package/dest/l1_tx_utils/l1_tx_utils.d.ts +7 -1
  64. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  65. package/dest/l1_tx_utils/l1_tx_utils.js +82 -29
  66. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  67. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  68. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +49 -19
  69. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  70. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  71. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  72. package/dest/publisher_manager.d.ts +18 -6
  73. package/dest/publisher_manager.d.ts.map +1 -1
  74. package/dest/publisher_manager.js +44 -7
  75. package/dest/queries.d.ts +13 -2
  76. package/dest/queries.d.ts.map +1 -1
  77. package/dest/queries.js +62 -3
  78. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  79. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  80. package/dest/test/blob_kzg_warmup.js +64 -0
  81. package/dest/test/chain_monitor.d.ts +34 -2
  82. package/dest/test/chain_monitor.d.ts.map +1 -1
  83. package/dest/test/chain_monitor.js +76 -6
  84. package/dest/test/eth_cheat_codes.d.ts +26 -3
  85. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  86. package/dest/test/eth_cheat_codes.js +61 -40
  87. package/dest/test/index.d.ts +2 -1
  88. package/dest/test/index.d.ts.map +1 -1
  89. package/dest/test/index.js +1 -0
  90. package/dest/test/recording_rpc_server.d.ts +18 -0
  91. package/dest/test/recording_rpc_server.d.ts.map +1 -0
  92. package/dest/test/recording_rpc_server.js +68 -0
  93. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  94. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  95. package/dest/test/rollup_cheat_codes.js +86 -7
  96. package/dest/test/start_anvil.d.ts +1 -1
  97. package/dest/test/start_anvil.d.ts.map +1 -1
  98. package/dest/test/start_anvil.js +41 -5
  99. package/dest/utils.d.ts +9 -2
  100. package/dest/utils.d.ts.map +1 -1
  101. package/dest/utils.js +45 -14
  102. package/package.json +6 -6
  103. package/src/client.ts +63 -1
  104. package/src/config.ts +44 -13
  105. package/src/contracts/chain_state_override.ts +200 -0
  106. package/src/contracts/empire_base.ts +0 -2
  107. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  108. package/src/contracts/governance.ts +310 -10
  109. package/src/contracts/governance_proposer.ts +56 -13
  110. package/src/contracts/gse.ts +7 -2
  111. package/src/contracts/inbox.ts +12 -4
  112. package/src/contracts/index.ts +2 -2
  113. package/src/contracts/multicall.ts +179 -105
  114. package/src/contracts/outbox.ts +42 -8
  115. package/src/contracts/rollup.ts +302 -119
  116. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +119 -45
  117. package/src/contracts/watch_event.ts +110 -0
  118. package/src/deploy_aztec_l1_contracts.ts +19 -10
  119. package/src/foundry_binary.ts +57 -0
  120. package/src/generated/l1-contracts-defaults.ts +2 -2
  121. package/src/l1_artifacts.ts +12 -35
  122. package/src/l1_contract_addresses.ts +120 -94
  123. package/src/l1_reader.ts +4 -9
  124. package/src/l1_tx_utils/config.ts +3 -3
  125. package/src/l1_tx_utils/l1_tx_utils.ts +83 -36
  126. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +49 -13
  127. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  128. package/src/publisher_manager.ts +54 -18
  129. package/src/queries.ts +70 -4
  130. package/src/test/blob_kzg_warmup.ts +65 -0
  131. package/src/test/chain_monitor.ts +93 -5
  132. package/src/test/eth_cheat_codes.ts +57 -43
  133. package/src/test/index.ts +1 -0
  134. package/src/test/recording_rpc_server.ts +69 -0
  135. package/src/test/rollup_cheat_codes.ts +115 -6
  136. package/src/test/start_anvil.ts +38 -6
  137. package/src/utils.ts +41 -17
  138. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  139. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  140. package/dest/contracts/empire_slashing_proposer.js +0 -216
  141. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  142. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  143. package/dest/contracts/tally_slashing_proposer.js +0 -320
  144. package/src/contracts/empire_slashing_proposer.ts +0 -265
@@ -6,6 +6,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import type { ViemSignature } from '@aztec/foundation/eth-signature';
7
7
  import { createLogger } from '@aztec/foundation/log';
8
8
  import { makeBackoff, retry } from '@aztec/foundation/retry';
9
+ import { getErrorCause } from '@aztec/foundation/types';
9
10
  import { EscapeHatchAbi } from '@aztec/l1-artifacts/EscapeHatchAbi';
10
11
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
11
12
  import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
@@ -13,10 +14,14 @@ import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
13
14
  import chunk from 'lodash.chunk';
14
15
  import {
15
16
  type Account,
17
+ ContractFunctionRevertedError,
16
18
  type GetContractReturnType,
17
19
  type Hex,
20
+ type Log,
21
+ RpcRequestError,
18
22
  type StateOverride,
19
23
  type WatchContractEventReturnType,
24
+ decodeErrorResult,
20
25
  encodeAbiParameters,
21
26
  encodeFunctionData,
22
27
  getContract,
@@ -31,12 +36,12 @@ import type { L1ReaderConfig } from '../l1_reader.js';
31
36
  import type { L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
32
37
  import type { ViemClient } from '../types.js';
33
38
  import { formatViemError } from '../utils.js';
34
- import { EmpireSlashingProposerContract } from './empire_slashing_proposer.js';
35
39
  import { GSEContract } from './gse.js';
36
40
  import type { L1EventLog } from './log.js';
37
41
  import { SlasherContract } from './slasher_contract.js';
38
- import { TallySlashingProposerContract } from './tally_slashing_proposer.js';
42
+ import { SlashingProposerContract } from './slashing_proposer.js';
39
43
  import { checkBlockTag } from './utils.js';
44
+ import { type WatchContractEventOptions, watchContractEvent } from './watch_event.js';
40
45
 
41
46
  export type ViemCommitteeAttestation = {
42
47
  addr: `0x${string}`;
@@ -57,7 +62,6 @@ export type L1RollupContractAddresses = Pick<
57
62
  | 'feeJuiceAddress'
58
63
  | 'stakingAssetAddress'
59
64
  | 'rewardDistributorAddress'
60
- | 'slashFactoryAddress'
61
65
  | 'gseAddress'
62
66
  >;
63
67
 
@@ -80,6 +84,7 @@ export type ViemHeader = {
80
84
  feeRecipient: `0x${string}`;
81
85
  gasFees: ViemGasFees;
82
86
  totalManaUsed: bigint;
87
+ accumulatedFees: bigint;
83
88
  };
84
89
 
85
90
  export type ViemGasFees = {
@@ -87,12 +92,6 @@ export type ViemGasFees = {
87
92
  feePerL2Gas: bigint;
88
93
  };
89
94
 
90
- export enum SlashingProposerType {
91
- None = 0,
92
- Tally = 1,
93
- Empire = 2,
94
- }
95
-
96
95
  /**
97
96
  * Status of a validator/attester in the staking system.
98
97
  * Matches the Status enum in StakingLib.sol
@@ -136,6 +135,31 @@ export type L1FeeData = {
136
135
  blobFee: bigint;
137
136
  };
138
137
 
138
+ /** Field offsets within the CompressedTempCheckpointLog struct in Solidity storage. */
139
+ export enum TempCheckpointLogField {
140
+ HeaderHash = 0,
141
+ BlobCommitmentsHash = 1,
142
+ OutHash = 2,
143
+ AttestationsHash = 3,
144
+ PayloadDigest = 4,
145
+ SlotNumber = 5,
146
+ FeeHeader = 6,
147
+ }
148
+
149
+ /**
150
+ * Field-level override input for `tempCheckpointLogs[checkpointNumber]`. Covers the fields the
151
+ * `propose()` path actually reads back. `payloadDigest` is `Buffer32` because it carries an
152
+ * arbitrary `bytes32` value rather than a BN254 scalar. `slotNumber` carries the uint32 portion
153
+ * of the on-chain `CompressedSlot`.
154
+ */
155
+ export type TempCheckpointLogOverrideFields = {
156
+ headerHash?: Fr;
157
+ outHash?: Fr;
158
+ payloadDigest?: Buffer32;
159
+ slotNumber?: SlotNumber;
160
+ feeHeader?: FeeHeader;
161
+ };
162
+
139
163
  /** Components of the minimum fee per mana, as returned by the L1 rollup contract. */
140
164
  export type ManaMinFeeComponents = {
141
165
  sequencerCost: bigint;
@@ -212,6 +236,35 @@ export type CheckpointProposedArgs = {
212
236
  /** Log type for CheckpointProposed events. */
213
237
  export type CheckpointProposedLog = L1EventLog<CheckpointProposedArgs>;
214
238
 
239
+ const INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR = 'ValidatorSelection__InsufficientValidatorSetSize';
240
+
241
+ /** SlasherUpdated events are rare governance operations, so their watcher polls well below the client's interval. */
242
+ const SLASHER_UPDATED_POLLING_INTERVAL_MS = 60_000;
243
+
244
+ function isValidatorSelectionError(err: unknown, errorName: string): boolean {
245
+ return (
246
+ getErrorCause(err, ContractFunctionRevertedError)?.data?.errorName === errorName ||
247
+ decodeRpcRequestErrorName(err) === errorName
248
+ );
249
+ }
250
+
251
+ function decodeRpcRequestErrorName(err: unknown): string | undefined {
252
+ const data = getErrorCause(err, RpcRequestError)?.data;
253
+ if (!isHexString(data)) {
254
+ return undefined;
255
+ }
256
+
257
+ try {
258
+ return decodeErrorResult({ abi: RollupAbi, data }).errorName;
259
+ } catch {
260
+ return undefined;
261
+ }
262
+ }
263
+
264
+ function isHexString(value: unknown): value is Hex {
265
+ return typeof value === 'string' && value.startsWith('0x');
266
+ }
267
+
215
268
  export class RollupContract {
216
269
  private readonly rollup: GetContractReturnType<typeof RollupAbi, ViemClient>;
217
270
  private readonly logger = createLogger('ethereum:rollup');
@@ -244,7 +297,7 @@ export class RollupContract {
244
297
 
245
298
  static getFromConfig(config: L1ReaderConfig) {
246
299
  const client = getPublicClient(config);
247
- const address = config.l1Contracts.rollupAddress.toString();
300
+ const address = config.rollupAddress.toString();
248
301
  return new RollupContract(client, address);
249
302
  }
250
303
 
@@ -270,34 +323,18 @@ export class RollupContract {
270
323
  return this.rollup;
271
324
  }
272
325
 
273
- public async getSlashingProposer(): Promise<
274
- EmpireSlashingProposerContract | TallySlashingProposerContract | undefined
275
- > {
326
+ public async getSlashingProposer(): Promise<SlashingProposerContract | undefined> {
276
327
  const slasher = await this.getSlasherContract();
277
328
  if (!slasher) {
278
329
  return undefined;
279
330
  }
280
331
 
281
332
  const proposerAddress = await slasher.getProposer();
282
- const proposerAbi = [
283
- {
284
- type: 'function',
285
- name: 'SLASHING_PROPOSER_TYPE',
286
- inputs: [],
287
- outputs: [{ name: '', type: 'uint8', internalType: 'enum SlasherFlavor' }],
288
- stateMutability: 'view',
289
- },
290
- ] as const;
291
-
292
- const proposer = getContract({ address: proposerAddress.toString(), abi: proposerAbi, client: this.client });
293
- const proposerType = await proposer.read.SLASHING_PROPOSER_TYPE();
294
- if (proposerType === SlashingProposerType.Tally.valueOf()) {
295
- return new TallySlashingProposerContract(this.client, proposerAddress);
296
- } else if (proposerType === SlashingProposerType.Empire.valueOf()) {
297
- return new EmpireSlashingProposerContract(this.client, proposerAddress);
298
- } else {
299
- throw new Error(`Unknown slashing proposer type: ${proposerType}`);
333
+ if (proposerAddress.isZero()) {
334
+ return undefined;
300
335
  }
336
+
337
+ return new SlashingProposerContract(this.client, proposerAddress);
301
338
  }
302
339
 
303
340
  @memoize
@@ -522,8 +559,9 @@ export class RollupContract {
522
559
  return EthAddress.fromString(await this.rollup.read.owner());
523
560
  }
524
561
 
525
- async getActiveAttesterCount(): Promise<number> {
526
- return Number(await this.rollup.read.getActiveAttesterCount());
562
+ async getActiveAttesterCount(options?: { blockNumber?: bigint }): Promise<number> {
563
+ await checkBlockTag(options?.blockNumber, this.client);
564
+ return Number(await this.rollup.read.getActiveAttesterCount(options));
527
565
  }
528
566
 
529
567
  public async getSlashingProposerAddress() {
@@ -538,8 +576,9 @@ export class RollupContract {
538
576
  return this.rollup.read.getCheckpointReward();
539
577
  }
540
578
 
541
- async getCheckpointNumber(): Promise<CheckpointNumber> {
542
- return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber());
579
+ async getCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
580
+ await checkBlockTag(options?.blockNumber, this.client);
581
+ return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber(options));
543
582
  }
544
583
 
545
584
  async getProvenCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
@@ -547,18 +586,31 @@ export class RollupContract {
547
586
  return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber(options));
548
587
  }
549
588
 
550
- async getSlotNumber(): Promise<SlotNumber> {
551
- return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot());
589
+ async getSlotNumber(options?: { blockNumber?: bigint }): Promise<SlotNumber> {
590
+ await checkBlockTag(options?.blockNumber, this.client);
591
+ return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot(options));
552
592
  }
553
593
 
554
- async getL1FeesAt(timestamp: bigint): Promise<L1FeeData> {
555
- const result = await this.rollup.read.getL1FeesAt([timestamp]);
594
+ async getL1FeesAt(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<L1FeeData> {
595
+ await checkBlockTag(options?.blockNumber, this.client);
596
+ const result = await this.rollup.read.getL1FeesAt([timestamp], options);
556
597
  return {
557
598
  baseFee: result.baseFee,
558
599
  blobFee: result.blobFee,
559
600
  };
560
601
  }
561
602
 
603
+ async getFeeHeader(checkpointNumber: bigint): Promise<FeeHeader> {
604
+ const result = await this.rollup.read.getFeeHeader([checkpointNumber]);
605
+ return {
606
+ excessMana: result.excessMana,
607
+ manaUsed: result.manaUsed,
608
+ ethPerFeeAsset: result.ethPerFeeAsset,
609
+ congestionCost: result.congestionCost,
610
+ proverCost: result.proverCost,
611
+ };
612
+ }
613
+
562
614
  getEthPerFeeAsset(): Promise<bigint> {
563
615
  return this.rollup.read.getEthPerFeeAsset();
564
616
  }
@@ -572,7 +624,7 @@ export class RollupContract {
572
624
  args: [timestamp],
573
625
  })
574
626
  .catch(e => {
575
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
627
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
576
628
  return { result: undefined };
577
629
  }
578
630
  throw e;
@@ -602,7 +654,7 @@ export class RollupContract {
602
654
  args: [],
603
655
  })
604
656
  .catch(e => {
605
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
657
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
606
658
  return { result: undefined };
607
659
  }
608
660
  throw e;
@@ -633,8 +685,9 @@ export class RollupContract {
633
685
  return EthAddress.fromString(result);
634
686
  }
635
687
 
636
- async getCheckpoint(checkpointNumber: CheckpointNumber): Promise<CheckpointLog> {
637
- const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)]);
688
+ async getCheckpoint(checkpointNumber: CheckpointNumber, options?: { blockNumber?: bigint }): Promise<CheckpointLog> {
689
+ await checkBlockTag(options?.blockNumber, this.client);
690
+ const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)], options);
638
691
  return {
639
692
  archive: Fr.fromString(result.archive),
640
693
  headerHash: Buffer32.fromString(result.headerHash),
@@ -667,6 +720,30 @@ export class RollupContract {
667
720
  );
668
721
  }
669
722
 
723
+ /**
724
+ * Returns the effective pending checkpoint, accounting for potential prunes.
725
+ * When a prune can happen, the L1 contract uses the proven checkpoint instead of the pending one.
726
+ * This mirrors the behavior of getEffectivePendingCheckpointNumber in STFLib.sol.
727
+ * @param atTimestamp - The timestamp to evaluate pruneability at. Defaults to the current L1 block timestamp.
728
+ * @param options - Optional L1 block number to pin the queries to.
729
+ */
730
+ getEffectivePendingCheckpoint(atTimestamp?: bigint, options?: { blockNumber?: bigint }) {
731
+ return retry(
732
+ async () => {
733
+ const timestamp = atTimestamp ?? (await this.client.getBlock()).timestamp;
734
+ const canPrune = await this.canPruneAtTime(timestamp, options);
735
+ if (canPrune) {
736
+ const provenCheckpointNumber = await this.getProvenCheckpointNumber(options);
737
+ return await this.getCheckpoint(provenCheckpointNumber, options);
738
+ }
739
+ const pendingCheckpointNumber = await this.getCheckpointNumber(options);
740
+ return await this.getCheckpoint(pendingCheckpointNumber, options);
741
+ },
742
+ 'getting effective pending checkpoint',
743
+ makeBackoff([0.5, 0.5, 0.5]),
744
+ );
745
+ }
746
+
670
747
  async getTips(): Promise<{ pending: CheckpointNumber; proven: CheckpointNumber }> {
671
748
  const { pending, proven } = await this.rollup.read.getTips();
672
749
  return {
@@ -741,7 +818,7 @@ export class RollupContract {
741
818
  }
742
819
 
743
820
  async getEpochProofPublicInputs(
744
- args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly `0x${string}`[], `0x${string}`],
821
+ args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly ViemHeader[], `0x${string}`],
745
822
  ): Promise<Fr[]> {
746
823
  const result = await this.rollup.read.getEpochProofPublicInputs(args);
747
824
  return result.map(Fr.fromString);
@@ -789,19 +866,11 @@ export class RollupContract {
789
866
  archive: Buffer,
790
867
  account: `0x${string}` | Account,
791
868
  timestamp: bigint,
792
- opts: {
793
- forcePendingCheckpointNumber?: CheckpointNumber;
794
- forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
795
- } = {},
869
+ stateOverride: StateOverride = [],
796
870
  ): Promise<{ slot: SlotNumber; checkpointNumber: CheckpointNumber; timeOfNextL1Slot: bigint }> {
797
871
  const timeOfNextL1Slot = timestamp;
798
872
  const who = typeof account === 'string' ? account : account.address;
799
873
 
800
- const stateOverride = RollupContract.mergeStateOverrides(
801
- await this.makePendingCheckpointNumberOverride(opts.forcePendingCheckpointNumber),
802
- opts.forceArchive ? this.makeArchiveOverride(opts.forceArchive.checkpointNumber, opts.forceArchive.archive) : [],
803
- );
804
-
805
874
  try {
806
875
  const {
807
876
  result: [slot, checkpointNumber],
@@ -825,20 +894,32 @@ export class RollupContract {
825
894
  }
826
895
 
827
896
  /**
828
- * Returns a state override that sets the pending checkpoint number to the specified value. Useful for simulations.
829
- * Requires querying the current state of the contract to get the current proven checkpoint number, as they are both
830
- * stored in the same slot. If the argument is undefined, it returns an empty override.
897
+ * Returns a state override that sets the pending and/or proven checkpoint numbers. Useful for simulations.
898
+ * Both values share a single storage slot (pending in the upper 128 bits, proven in the lower 128 bits), so
899
+ * a single combined override is emitted to avoid the second state-diff clobbering the first. The current live
900
+ * value is read once to preserve any half not being overridden. Returns an empty override if neither is set.
901
+ *
902
+ * Throws if the resulting `proven > pending`, which would crash the simulation: `STFLib.canPruneAtTime` calls
903
+ * `getEpochForCheckpoint(proven + 1)` whenever `pending != proven`, and that asserts `_n <= tips.pending`.
831
904
  */
832
- public async makePendingCheckpointNumberOverride(
833
- forcePendingCheckpointNumber: CheckpointNumber | undefined,
834
- ): Promise<StateOverride> {
835
- if (forcePendingCheckpointNumber === undefined) {
905
+ public async makeChainTipsOverride(override: {
906
+ pending?: CheckpointNumber;
907
+ proven?: CheckpointNumber;
908
+ }): Promise<StateOverride> {
909
+ if (override.pending === undefined && override.proven === undefined) {
836
910
  return [];
837
911
  }
838
912
  const slot = RollupContract.stfStorageSlot;
839
913
  const currentValue = await this.client.getStorageAt({ address: this.address, slot });
840
- const currentProvenCheckpointNumber = currentValue ? hexToBigInt(currentValue) & ((1n << 128n) - 1n) : 0n;
841
- const newValue = (BigInt(forcePendingCheckpointNumber) << 128n) | currentProvenCheckpointNumber;
914
+ const currentRaw = currentValue ? hexToBigInt(currentValue) : 0n;
915
+ const currentPending = currentRaw >> 128n;
916
+ const currentProven = currentRaw & ((1n << 128n) - 1n);
917
+ const newPending = override.pending !== undefined ? BigInt(override.pending) : currentPending;
918
+ const newProven = override.proven !== undefined ? BigInt(override.proven) : currentProven;
919
+ if (newProven > newPending) {
920
+ throw new Error(`Invalid chain tips override: proven (${newProven}) > pending (${newPending})`);
921
+ }
922
+ const newValue = (newPending << 128n) | newProven;
842
923
  return [
843
924
  {
844
925
  address: this.address,
@@ -848,39 +929,56 @@ export class RollupContract {
848
929
  }
849
930
 
850
931
  /**
851
- * Returns a state override that sets tempCheckpointLogs[checkpointNumber].feeHeader to the compressed fee header.
852
- * Used when simulating a propose call where the parent checkpoint hasn't landed on L1 yet (pipelining).
932
+ * Returns a state override that patches `tempCheckpointLogs[checkpointNumber]` with every field that
933
+ * the L1 contract reads during `propose()` for the checkpoint that builds on top of it (proposer
934
+ * pipelining simulation). Mirrors the writes done by `ProposeLib.addTempCheckpointLog`.
935
+ *
936
+ * `blobCommitmentsHash` and `attestationsHash` are intentionally not exposed here — the propose path
937
+ * never asserts against them, so leaving them at storage zero is harmless.
853
938
  */
854
- public async makeFeeHeaderOverride(checkpointNumber: CheckpointNumber, feeHeader: FeeHeader): Promise<StateOverride> {
855
- const { epochDuration, proofSubmissionEpochs } = await this.getRollupConstants();
856
- const roundaboutSize = BigInt(epochDuration * (proofSubmissionEpochs + 1) + 1);
857
- const circularIndex = BigInt(checkpointNumber) % roundaboutSize;
858
-
859
- // tempCheckpointLogs is at offset 2 in RollupStore
860
- const tempCheckpointLogsMappingBase = hexToBigInt(RollupContract.stfStorageSlot) + 2n;
939
+ public async makeTempCheckpointLogOverride(
940
+ checkpointNumber: CheckpointNumber,
941
+ fields: TempCheckpointLogOverrideFields,
942
+ ): Promise<StateOverride> {
943
+ const constants = await this.getRollupConstants();
944
+ const slotAt = (field: TempCheckpointLogField) =>
945
+ `0x${this.computeTempCheckpointLogStorageSlot(checkpointNumber, field, constants).toString(16).padStart(64, '0')}` as const;
946
+ const word = (v: bigint) => `0x${v.toString(16).padStart(64, '0')}` as const;
861
947
 
862
- // Solidity mapping slot: keccak256(abi.encode(key, baseSlot))
863
- const structBaseSlot = hexToBigInt(
864
- keccak256(
865
- encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [circularIndex, tempCheckpointLogsMappingBase]),
866
- ),
867
- );
948
+ const stateDiff: { slot: `0x${string}`; value: `0x${string}` }[] = [];
868
949
 
869
- // feeHeader is the 7th field (offset 6) in CompressedTempCheckpointLog
870
- const feeHeaderSlot = structBaseSlot + 6n;
871
- const compressed = RollupContract.compressFeeHeader(feeHeader);
950
+ if (fields.headerHash) {
951
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.HeaderHash), value: fields.headerHash.toString() });
952
+ }
953
+ if (fields.outHash) {
954
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.OutHash), value: fields.outHash.toString() });
955
+ }
956
+ if (fields.payloadDigest) {
957
+ stateDiff.push({
958
+ slot: slotAt(TempCheckpointLogField.PayloadDigest),
959
+ value: fields.payloadDigest.toString() as `0x${string}`,
960
+ });
961
+ }
962
+ if (fields.slotNumber !== undefined) {
963
+ // CompressedSlot is uint32 on L1 (SafeCast.toUint32 reverts on overflow). Match that behavior here
964
+ // so a malformed override surfaces immediately rather than silently truncating into a wrong slot.
965
+ const slotNumber = BigInt(fields.slotNumber);
966
+ if (slotNumber < 0n || slotNumber > 0xffffffffn) {
967
+ throw new Error(`slotNumber ${slotNumber} does not fit in uint32`);
968
+ }
969
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.SlotNumber), value: word(slotNumber) });
970
+ }
971
+ if (fields.feeHeader) {
972
+ stateDiff.push({
973
+ slot: slotAt(TempCheckpointLogField.FeeHeader),
974
+ value: word(RollupContract.compressFeeHeader(fields.feeHeader)),
975
+ });
976
+ }
872
977
 
873
- return [
874
- {
875
- address: this.address,
876
- stateDiff: [
877
- {
878
- slot: `0x${feeHeaderSlot.toString(16).padStart(64, '0')}`,
879
- value: `0x${compressed.toString(16).padStart(64, '0')}`,
880
- },
881
- ],
882
- },
883
- ];
978
+ if (stateDiff.length === 0) {
979
+ return [];
980
+ }
981
+ return [{ address: this.address, stateDiff }];
884
982
  }
885
983
 
886
984
  /**
@@ -1098,13 +1196,20 @@ export class RollupContract {
1098
1196
  }
1099
1197
 
1100
1198
  async getAttesters(timestamp?: bigint): Promise<EthAddress[]> {
1101
- const attesterSize = await this.getActiveAttesterCount();
1199
+ // Pin every read to a single L1 block so the attester count and the chunked index reads
1200
+ // observe a consistent set. Without this, the count and each chunk default to `latest` and
1201
+ // can straddle a block boundary (or reorg), yielding an inconsistent or truncated set.
1202
+ const block = await this.client.getBlock();
1203
+ const blockNumber = block.number ?? undefined;
1204
+ const ts = timestamp ?? block.timestamp;
1205
+ const attesterSize = await this.getActiveAttesterCount({ blockNumber });
1102
1206
  const gse = new GSEContract(this.client, await this.getGSE());
1103
- const ts = timestamp ?? (await this.client.getBlock()).timestamp;
1104
1207
  const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
1105
1208
  const chunks = chunk(indices, 1000);
1106
1209
 
1107
- const results = await Promise.all(chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk)));
1210
+ const results = await Promise.all(
1211
+ chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk, { blockNumber })),
1212
+ );
1108
1213
  return results.flat().map(addr => EthAddress.fromString(addr));
1109
1214
  }
1110
1215
 
@@ -1187,39 +1292,57 @@ export class RollupContract {
1187
1292
  });
1188
1293
  }
1189
1294
 
1295
+ /**
1296
+ * Watches for SlasherUpdated events. Events are delivered by polling `eth_getLogs`: a reorg may re-emit them and
1297
+ * removals are never reported, and events mined within roughly one polling interval of subscribing may be missed.
1298
+ * Slasher rotations are rare governance operations, so by default this polls much slower than the other watchers.
1299
+ */
1190
1300
  public listenToSlasherChanged(
1191
1301
  callback: (args: { oldSlasher: `0x${string}`; newSlasher: `0x${string}` }) => unknown,
1302
+ options?: WatchContractEventOptions,
1192
1303
  ): WatchContractEventReturnType {
1193
- return this.rollup.watchEvent.SlasherUpdated(
1194
- {},
1304
+ return watchContractEvent(
1305
+ this.client,
1306
+ this.logger,
1195
1307
  {
1196
- onLogs: logs => {
1197
- for (const log of logs) {
1198
- const args = log.args;
1199
- if (args.oldSlasher && args.newSlasher) {
1200
- callback(args as { oldSlasher: `0x${string}`; newSlasher: `0x${string}` });
1201
- }
1308
+ address: this.address,
1309
+ abi: RollupAbi,
1310
+ eventName: 'SlasherUpdated',
1311
+ onLog: log => {
1312
+ const { oldSlasher, newSlasher } = log.args;
1313
+ if (oldSlasher && newSlasher) {
1314
+ return callback({ oldSlasher, newSlasher });
1202
1315
  }
1203
1316
  },
1204
1317
  },
1318
+ { pollingIntervalMs: SLASHER_UPDATED_POLLING_INTERVAL_MS, ...options },
1205
1319
  );
1206
1320
  }
1207
1321
 
1322
+ /**
1323
+ * Watches for CheckpointInvalidated events. Events are delivered by polling `eth_getLogs`: a reorg may re-emit
1324
+ * them and removals are never reported, and events mined within roughly one polling interval of subscribing may
1325
+ * be missed.
1326
+ */
1208
1327
  public listenToCheckpointInvalidated(
1209
- callback: (args: { checkpointNumber: CheckpointNumber }) => unknown,
1328
+ callback: (args: { checkpointNumber: CheckpointNumber; event: Log }) => unknown,
1329
+ options?: WatchContractEventOptions,
1210
1330
  ): WatchContractEventReturnType {
1211
- return this.rollup.watchEvent.CheckpointInvalidated(
1212
- {},
1331
+ return watchContractEvent(
1332
+ this.client,
1333
+ this.logger,
1213
1334
  {
1214
- onLogs: logs => {
1215
- for (const log of logs) {
1216
- const args = log.args;
1217
- if (args.checkpointNumber !== undefined) {
1218
- callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber) });
1219
- }
1335
+ address: this.address,
1336
+ abi: RollupAbi,
1337
+ eventName: 'CheckpointInvalidated',
1338
+ onLog: log => {
1339
+ const { checkpointNumber } = log.args;
1340
+ if (checkpointNumber !== undefined) {
1341
+ return callback({ checkpointNumber: CheckpointNumber.fromBigInt(checkpointNumber), event: log });
1220
1342
  }
1221
1343
  },
1222
1344
  },
1345
+ options,
1223
1346
  );
1224
1347
  }
1225
1348
 
@@ -1231,23 +1354,39 @@ export class RollupContract {
1231
1354
  }));
1232
1355
  }
1233
1356
 
1357
+ /**
1358
+ * Watches for Slashed events. Events are delivered by polling `eth_getLogs`: a reorg may re-emit them and
1359
+ * removals are never reported, and events mined within roughly one polling interval of subscribing may be missed.
1360
+ */
1234
1361
  public listenToSlash(
1235
1362
  callback: (args: { amount: bigint; attester: EthAddress }) => unknown,
1363
+ options?: WatchContractEventOptions,
1236
1364
  ): WatchContractEventReturnType {
1237
- return this.rollup.watchEvent.Slashed(
1238
- {},
1365
+ return watchContractEvent(
1366
+ this.client,
1367
+ this.logger,
1239
1368
  {
1369
+ address: this.address,
1370
+ abi: RollupAbi,
1371
+ eventName: 'Slashed',
1240
1372
  strict: true,
1241
- onLogs: logs => {
1242
- for (const log of logs) {
1243
- const args = log.args;
1244
- callback({ amount: args.amount!, attester: EthAddress.fromString(args.attester!) });
1245
- }
1246
- },
1373
+ onLog: log => callback({ amount: log.args.amount, attester: EthAddress.fromString(log.args.attester) }),
1247
1374
  },
1375
+ options,
1248
1376
  );
1249
1377
  }
1250
1378
 
1379
+ /**
1380
+ * Fetches OwnershipTransferred events emitted on the L1 block this rollup was deployed on.
1381
+ * The Rollup inherits from Ownable and emits this event in its constructor, so the event
1382
+ * is guaranteed to exist on `l1StartBlock` for any correctly deployed rollup. Used as a
1383
+ * probe to detect RPC nodes that prune historical logs.
1384
+ */
1385
+ async getOwnershipTransferredEventsAtDeploy() {
1386
+ const l1StartBlock = await this.getL1StartBlock();
1387
+ return await this.rollup.getEvents.OwnershipTransferred({}, { fromBlock: l1StartBlock, toBlock: l1StartBlock });
1388
+ }
1389
+
1251
1390
  /** Fetches CheckpointProposed events within the given block range. */
1252
1391
  async getCheckpointProposedEvents(fromBlock: bigint, toBlock: bigint): Promise<CheckpointProposedLog[]> {
1253
1392
  const logs = await this.rollup.getEvents.CheckpointProposed({}, { fromBlock, toBlock });
@@ -1280,4 +1419,48 @@ export class RollupContract {
1280
1419
  },
1281
1420
  }));
1282
1421
  }
1422
+
1423
+ /** Packs pending and proven checkpoint numbers into the chain tips storage format. */
1424
+ static packChainTips(pendingCheckpointNumber: bigint, provenCheckpointNumber: bigint): bigint {
1425
+ return (pendingCheckpointNumber << 128n) | (provenCheckpointNumber & ((1n << 128n) - 1n));
1426
+ }
1427
+
1428
+ /** Storage slot for the chain tips (offset 0 within the STF storage struct). */
1429
+ static get chainTipsStorageSlot(): bigint {
1430
+ return BigInt(RollupContract.stfStorageSlot);
1431
+ }
1432
+
1433
+ /**
1434
+ * Computes the storage slot for a field within a tempCheckpointLog entry.
1435
+ * @param checkpointNumber - The checkpoint number
1436
+ * @param field - The field within the CompressedTempCheckpointLog struct
1437
+ */
1438
+ async getTempCheckpointLogStorageSlot(
1439
+ checkpointNumber: CheckpointNumber,
1440
+ field: TempCheckpointLogField,
1441
+ ): Promise<bigint> {
1442
+ const [epochDuration, proofSubmissionEpochs] = await Promise.all([
1443
+ this.getEpochDuration(),
1444
+ this.getProofSubmissionEpochs(),
1445
+ ]);
1446
+ return this.computeTempCheckpointLogStorageSlot(checkpointNumber, field, { epochDuration, proofSubmissionEpochs });
1447
+ }
1448
+
1449
+ private computeTempCheckpointLogStorageSlot(
1450
+ checkpointNumber: CheckpointNumber,
1451
+ field: TempCheckpointLogField,
1452
+ constants: { epochDuration: number; proofSubmissionEpochs: number },
1453
+ ): bigint {
1454
+ const fieldOffset = BigInt(field);
1455
+ const { epochDuration, proofSubmissionEpochs } = constants;
1456
+ const roundaboutSize = BigInt(epochDuration) * (BigInt(proofSubmissionEpochs) + 1n) + 1n;
1457
+ const tempCheckpointLogsBase = BigInt(RollupContract.stfStorageSlot) + 2n;
1458
+ const circularIndex = BigInt(checkpointNumber) % roundaboutSize;
1459
+ const entryBase = BigInt(
1460
+ keccak256(
1461
+ encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [circularIndex, tempCheckpointLogsBase]),
1462
+ ),
1463
+ );
1464
+ return entryBase + fieldOffset;
1465
+ }
1283
1466
  }