@aztec/ethereum 0.0.1-commit.b6e433891 → 0.0.1-commit.b8a057fa

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 (139) hide show
  1. package/dest/client.d.ts +17 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +49 -3
  4. package/dest/config.d.ts +27 -8
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +59 -12
  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 +108 -33
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +193 -12
  18. package/dest/contracts/governance_proposer.d.ts +33 -3
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +51 -9
  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 +12 -7
  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 +130 -13
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +203 -94
  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 -26
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +343 -69
  39. package/dest/contracts/{tally_slashing_proposer.d.ts → slashing_proposer.d.ts} +3 -4
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/{tally_slashing_proposer.js → slashing_proposer.js} +13 -15
  42. package/dest/deploy_aztec_l1_contracts.d.ts +18 -10
  43. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  44. package/dest/deploy_aztec_l1_contracts.js +17 -13
  45. package/dest/l1_artifacts.d.ts +14941 -16273
  46. package/dest/l1_artifacts.d.ts.map +1 -1
  47. package/dest/l1_artifacts.js +9 -24
  48. package/dest/l1_contract_addresses.d.ts +61 -65
  49. package/dest/l1_contract_addresses.d.ts.map +1 -1
  50. package/dest/l1_contract_addresses.js +101 -82
  51. package/dest/l1_reader.d.ts +3 -5
  52. package/dest/l1_reader.d.ts.map +1 -1
  53. package/dest/l1_reader.js +3 -6
  54. package/dest/l1_tx_utils/config.js +3 -3
  55. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
  56. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
  57. package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
  58. package/dest/l1_tx_utils/l1_tx_utils.d.ts +9 -1
  59. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  60. package/dest/l1_tx_utils/l1_tx_utils.js +138 -57
  61. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  62. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  63. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
  64. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  65. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  66. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  67. package/dest/l1_tx_utils/types.d.ts +27 -1
  68. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  69. package/dest/l1_tx_utils/types.js +10 -0
  70. package/dest/publisher_manager.d.ts +34 -8
  71. package/dest/publisher_manager.d.ts.map +1 -1
  72. package/dest/publisher_manager.js +119 -8
  73. package/dest/queries.d.ts +13 -2
  74. package/dest/queries.d.ts.map +1 -1
  75. package/dest/queries.js +69 -4
  76. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  77. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  78. package/dest/test/blob_kzg_warmup.js +64 -0
  79. package/dest/test/chain_monitor.d.ts +34 -2
  80. package/dest/test/chain_monitor.d.ts.map +1 -1
  81. package/dest/test/chain_monitor.js +76 -6
  82. package/dest/test/eth_cheat_codes.d.ts +37 -6
  83. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  84. package/dest/test/eth_cheat_codes.js +89 -41
  85. package/dest/test/index.d.ts +2 -1
  86. package/dest/test/index.d.ts.map +1 -1
  87. package/dest/test/index.js +1 -0
  88. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  89. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  90. package/dest/test/rollup_cheat_codes.js +86 -7
  91. package/dest/test/start_anvil.d.ts +8 -1
  92. package/dest/test/start_anvil.d.ts.map +1 -1
  93. package/dest/test/start_anvil.js +16 -2
  94. package/dest/utils.d.ts +9 -2
  95. package/dest/utils.d.ts.map +1 -1
  96. package/dest/utils.js +34 -6
  97. package/package.json +6 -7
  98. package/src/client.ts +57 -1
  99. package/src/config.ts +82 -16
  100. package/src/contracts/chain_state_override.ts +200 -0
  101. package/src/contracts/empire_base.ts +0 -2
  102. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  103. package/src/contracts/governance.ts +266 -9
  104. package/src/contracts/governance_proposer.ts +52 -5
  105. package/src/contracts/gse.ts +7 -2
  106. package/src/contracts/inbox.ts +13 -5
  107. package/src/contracts/index.ts +2 -2
  108. package/src/contracts/multicall.ts +247 -106
  109. package/src/contracts/outbox.ts +42 -8
  110. package/src/contracts/rollup.ts +382 -69
  111. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +14 -16
  112. package/src/deploy_aztec_l1_contracts.ts +16 -14
  113. package/src/l1_artifacts.ts +12 -35
  114. package/src/l1_contract_addresses.ts +120 -94
  115. package/src/l1_reader.ts +4 -9
  116. package/src/l1_tx_utils/config.ts +3 -3
  117. package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
  118. package/src/l1_tx_utils/l1_tx_utils.ts +122 -48
  119. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
  120. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  121. package/src/l1_tx_utils/types.ts +32 -0
  122. package/src/publisher_manager.ts +143 -12
  123. package/src/queries.ts +76 -4
  124. package/src/test/blob_kzg_warmup.ts +65 -0
  125. package/src/test/chain_monitor.ts +93 -5
  126. package/src/test/eth_cheat_codes.ts +82 -45
  127. package/src/test/index.ts +1 -0
  128. package/src/test/rollup_cheat_codes.ts +115 -6
  129. package/src/test/start_anvil.ts +25 -2
  130. package/src/utils.ts +30 -6
  131. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  132. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  133. package/dest/contracts/empire_slashing_proposer.js +0 -216
  134. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  135. package/dest/generated/l1-contracts-defaults.d.ts +0 -30
  136. package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
  137. package/dest/generated/l1-contracts-defaults.js +0 -30
  138. package/src/contracts/empire_slashing_proposer.ts +0 -265
  139. package/src/generated/l1-contracts-defaults.ts +0 -32
@@ -4,7 +4,9 @@ import { Fr } from '@aztec/foundation/curves/bn254';
4
4
  import { memoize } from '@aztec/foundation/decorators';
5
5
  import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import type { ViemSignature } from '@aztec/foundation/eth-signature';
7
+ import { createLogger } from '@aztec/foundation/log';
7
8
  import { makeBackoff, retry } from '@aztec/foundation/retry';
9
+ import { getErrorCause } from '@aztec/foundation/types';
8
10
  import { EscapeHatchAbi } from '@aztec/l1-artifacts/EscapeHatchAbi';
9
11
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
10
12
  import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
@@ -12,10 +14,15 @@ import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
12
14
  import chunk from 'lodash.chunk';
13
15
  import {
14
16
  type Account,
17
+ ContractFunctionRevertedError,
15
18
  type GetContractReturnType,
16
19
  type Hex,
20
+ type Log,
21
+ RpcRequestError,
17
22
  type StateOverride,
18
23
  type WatchContractEventReturnType,
24
+ decodeErrorResult,
25
+ encodeAbiParameters,
19
26
  encodeFunctionData,
20
27
  getContract,
21
28
  hexToBigInt,
@@ -29,11 +36,10 @@ import type { L1ReaderConfig } from '../l1_reader.js';
29
36
  import type { L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
30
37
  import type { ViemClient } from '../types.js';
31
38
  import { formatViemError } from '../utils.js';
32
- import { EmpireSlashingProposerContract } from './empire_slashing_proposer.js';
33
39
  import { GSEContract } from './gse.js';
34
40
  import type { L1EventLog } from './log.js';
35
41
  import { SlasherContract } from './slasher_contract.js';
36
- import { TallySlashingProposerContract } from './tally_slashing_proposer.js';
42
+ import { SlashingProposerContract } from './slashing_proposer.js';
37
43
  import { checkBlockTag } from './utils.js';
38
44
 
39
45
  export type ViemCommitteeAttestation = {
@@ -55,7 +61,6 @@ export type L1RollupContractAddresses = Pick<
55
61
  | 'feeJuiceAddress'
56
62
  | 'stakingAssetAddress'
57
63
  | 'rewardDistributorAddress'
58
- | 'slashFactoryAddress'
59
64
  | 'gseAddress'
60
65
  >;
61
66
 
@@ -78,6 +83,7 @@ export type ViemHeader = {
78
83
  feeRecipient: `0x${string}`;
79
84
  gasFees: ViemGasFees;
80
85
  totalManaUsed: bigint;
86
+ accumulatedFees: bigint;
81
87
  };
82
88
 
83
89
  export type ViemGasFees = {
@@ -85,12 +91,6 @@ export type ViemGasFees = {
85
91
  feePerL2Gas: bigint;
86
92
  };
87
93
 
88
- export enum SlashingProposerType {
89
- None = 0,
90
- Tally = 1,
91
- Empire = 2,
92
- }
93
-
94
94
  /**
95
95
  * Status of a validator/attester in the staking system.
96
96
  * Matches the Status enum in StakingLib.sol
@@ -134,6 +134,31 @@ export type L1FeeData = {
134
134
  blobFee: bigint;
135
135
  };
136
136
 
137
+ /** Field offsets within the CompressedTempCheckpointLog struct in Solidity storage. */
138
+ export enum TempCheckpointLogField {
139
+ HeaderHash = 0,
140
+ BlobCommitmentsHash = 1,
141
+ OutHash = 2,
142
+ AttestationsHash = 3,
143
+ PayloadDigest = 4,
144
+ SlotNumber = 5,
145
+ FeeHeader = 6,
146
+ }
147
+
148
+ /**
149
+ * Field-level override input for `tempCheckpointLogs[checkpointNumber]`. Covers the fields the
150
+ * `propose()` path actually reads back. `payloadDigest` is `Buffer32` because it carries an
151
+ * arbitrary `bytes32` value rather than a BN254 scalar. `slotNumber` carries the uint32 portion
152
+ * of the on-chain `CompressedSlot`.
153
+ */
154
+ export type TempCheckpointLogOverrideFields = {
155
+ headerHash?: Fr;
156
+ outHash?: Fr;
157
+ payloadDigest?: Buffer32;
158
+ slotNumber?: SlotNumber;
159
+ feeHeader?: FeeHeader;
160
+ };
161
+
137
162
  /** Components of the minimum fee per mana, as returned by the L1 rollup contract. */
138
163
  export type ManaMinFeeComponents = {
139
164
  sequencerCost: bigint;
@@ -210,8 +235,35 @@ export type CheckpointProposedArgs = {
210
235
  /** Log type for CheckpointProposed events. */
211
236
  export type CheckpointProposedLog = L1EventLog<CheckpointProposedArgs>;
212
237
 
238
+ const INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR = 'ValidatorSelection__InsufficientValidatorSetSize';
239
+
240
+ function isValidatorSelectionError(err: unknown, errorName: string): boolean {
241
+ return (
242
+ getErrorCause(err, ContractFunctionRevertedError)?.data?.errorName === errorName ||
243
+ decodeRpcRequestErrorName(err) === errorName
244
+ );
245
+ }
246
+
247
+ function decodeRpcRequestErrorName(err: unknown): string | undefined {
248
+ const data = getErrorCause(err, RpcRequestError)?.data;
249
+ if (!isHexString(data)) {
250
+ return undefined;
251
+ }
252
+
253
+ try {
254
+ return decodeErrorResult({ abi: RollupAbi, data }).errorName;
255
+ } catch {
256
+ return undefined;
257
+ }
258
+ }
259
+
260
+ function isHexString(value: unknown): value is Hex {
261
+ return typeof value === 'string' && value.startsWith('0x');
262
+ }
263
+
213
264
  export class RollupContract {
214
265
  private readonly rollup: GetContractReturnType<typeof RollupAbi, ViemClient>;
266
+ private readonly logger = createLogger('ethereum:rollup');
215
267
 
216
268
  private static cachedStfStorageSlot: Hex | undefined;
217
269
  private cachedEscapeHatch?: {
@@ -241,7 +293,7 @@ export class RollupContract {
241
293
 
242
294
  static getFromConfig(config: L1ReaderConfig) {
243
295
  const client = getPublicClient(config);
244
- const address = config.l1Contracts.rollupAddress.toString();
296
+ const address = config.rollupAddress.toString();
245
297
  return new RollupContract(client, address);
246
298
  }
247
299
 
@@ -267,34 +319,18 @@ export class RollupContract {
267
319
  return this.rollup;
268
320
  }
269
321
 
270
- public async getSlashingProposer(): Promise<
271
- EmpireSlashingProposerContract | TallySlashingProposerContract | undefined
272
- > {
322
+ public async getSlashingProposer(): Promise<SlashingProposerContract | undefined> {
273
323
  const slasher = await this.getSlasherContract();
274
324
  if (!slasher) {
275
325
  return undefined;
276
326
  }
277
327
 
278
328
  const proposerAddress = await slasher.getProposer();
279
- const proposerAbi = [
280
- {
281
- type: 'function',
282
- name: 'SLASHING_PROPOSER_TYPE',
283
- inputs: [],
284
- outputs: [{ name: '', type: 'uint8', internalType: 'enum SlasherFlavor' }],
285
- stateMutability: 'view',
286
- },
287
- ] as const;
288
-
289
- const proposer = getContract({ address: proposerAddress.toString(), abi: proposerAbi, client: this.client });
290
- const proposerType = await proposer.read.SLASHING_PROPOSER_TYPE();
291
- if (proposerType === SlashingProposerType.Tally.valueOf()) {
292
- return new TallySlashingProposerContract(this.client, proposerAddress);
293
- } else if (proposerType === SlashingProposerType.Empire.valueOf()) {
294
- return new EmpireSlashingProposerContract(this.client, proposerAddress);
295
- } else {
296
- throw new Error(`Unknown slashing proposer type: ${proposerType}`);
329
+ if (proposerAddress.isZero()) {
330
+ return undefined;
297
331
  }
332
+
333
+ return new SlashingProposerContract(this.client, proposerAddress);
298
334
  }
299
335
 
300
336
  @memoize
@@ -495,7 +531,11 @@ export class RollupContract {
495
531
 
496
532
  const [isOpen] = await escapeHatch.read.isHatchOpen([BigInt(epoch)]);
497
533
  return isOpen;
498
- } catch {
534
+ } catch (err) {
535
+ this.logger.warn('isEscapeHatchOpen failed (treating as closed); RPC or contract error may cause liveness risk', {
536
+ epoch: Number(epoch),
537
+ error: err,
538
+ });
499
539
  return false;
500
540
  }
501
541
  }
@@ -515,8 +555,9 @@ export class RollupContract {
515
555
  return EthAddress.fromString(await this.rollup.read.owner());
516
556
  }
517
557
 
518
- async getActiveAttesterCount(): Promise<number> {
519
- return Number(await this.rollup.read.getActiveAttesterCount());
558
+ async getActiveAttesterCount(options?: { blockNumber?: bigint }): Promise<number> {
559
+ await checkBlockTag(options?.blockNumber, this.client);
560
+ return Number(await this.rollup.read.getActiveAttesterCount(options));
520
561
  }
521
562
 
522
563
  public async getSlashingProposerAddress() {
@@ -531,8 +572,9 @@ export class RollupContract {
531
572
  return this.rollup.read.getCheckpointReward();
532
573
  }
533
574
 
534
- async getCheckpointNumber(): Promise<CheckpointNumber> {
535
- return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber());
575
+ async getCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
576
+ await checkBlockTag(options?.blockNumber, this.client);
577
+ return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber(options));
536
578
  }
537
579
 
538
580
  async getProvenCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
@@ -540,18 +582,31 @@ export class RollupContract {
540
582
  return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber(options));
541
583
  }
542
584
 
543
- async getSlotNumber(): Promise<SlotNumber> {
544
- return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot());
585
+ async getSlotNumber(options?: { blockNumber?: bigint }): Promise<SlotNumber> {
586
+ await checkBlockTag(options?.blockNumber, this.client);
587
+ return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot(options));
545
588
  }
546
589
 
547
- async getL1FeesAt(timestamp: bigint): Promise<L1FeeData> {
548
- const result = await this.rollup.read.getL1FeesAt([timestamp]);
590
+ async getL1FeesAt(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<L1FeeData> {
591
+ await checkBlockTag(options?.blockNumber, this.client);
592
+ const result = await this.rollup.read.getL1FeesAt([timestamp], options);
549
593
  return {
550
594
  baseFee: result.baseFee,
551
595
  blobFee: result.blobFee,
552
596
  };
553
597
  }
554
598
 
599
+ async getFeeHeader(checkpointNumber: bigint): Promise<FeeHeader> {
600
+ const result = await this.rollup.read.getFeeHeader([checkpointNumber]);
601
+ return {
602
+ excessMana: result.excessMana,
603
+ manaUsed: result.manaUsed,
604
+ ethPerFeeAsset: result.ethPerFeeAsset,
605
+ congestionCost: result.congestionCost,
606
+ proverCost: result.proverCost,
607
+ };
608
+ }
609
+
555
610
  getEthPerFeeAsset(): Promise<bigint> {
556
611
  return this.rollup.read.getEthPerFeeAsset();
557
612
  }
@@ -565,7 +620,7 @@ export class RollupContract {
565
620
  args: [timestamp],
566
621
  })
567
622
  .catch(e => {
568
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
623
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
569
624
  return { result: undefined };
570
625
  }
571
626
  throw e;
@@ -595,7 +650,7 @@ export class RollupContract {
595
650
  args: [],
596
651
  })
597
652
  .catch(e => {
598
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
653
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
599
654
  return { result: undefined };
600
655
  }
601
656
  throw e;
@@ -626,8 +681,9 @@ export class RollupContract {
626
681
  return EthAddress.fromString(result);
627
682
  }
628
683
 
629
- async getCheckpoint(checkpointNumber: CheckpointNumber): Promise<CheckpointLog> {
630
- const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)]);
684
+ async getCheckpoint(checkpointNumber: CheckpointNumber, options?: { blockNumber?: bigint }): Promise<CheckpointLog> {
685
+ await checkBlockTag(options?.blockNumber, this.client);
686
+ const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)], options);
631
687
  return {
632
688
  archive: Fr.fromString(result.archive),
633
689
  headerHash: Buffer32.fromString(result.headerHash),
@@ -660,6 +716,30 @@ export class RollupContract {
660
716
  );
661
717
  }
662
718
 
719
+ /**
720
+ * Returns the effective pending checkpoint, accounting for potential prunes.
721
+ * When a prune can happen, the L1 contract uses the proven checkpoint instead of the pending one.
722
+ * This mirrors the behavior of getEffectivePendingCheckpointNumber in STFLib.sol.
723
+ * @param atTimestamp - The timestamp to evaluate pruneability at. Defaults to the current L1 block timestamp.
724
+ * @param options - Optional L1 block number to pin the queries to.
725
+ */
726
+ getEffectivePendingCheckpoint(atTimestamp?: bigint, options?: { blockNumber?: bigint }) {
727
+ return retry(
728
+ async () => {
729
+ const timestamp = atTimestamp ?? (await this.client.getBlock()).timestamp;
730
+ const canPrune = await this.canPruneAtTime(timestamp, options);
731
+ if (canPrune) {
732
+ const provenCheckpointNumber = await this.getProvenCheckpointNumber(options);
733
+ return await this.getCheckpoint(provenCheckpointNumber, options);
734
+ }
735
+ const pendingCheckpointNumber = await this.getCheckpointNumber(options);
736
+ return await this.getCheckpoint(pendingCheckpointNumber, options);
737
+ },
738
+ 'getting effective pending checkpoint',
739
+ makeBackoff([0.5, 0.5, 0.5]),
740
+ );
741
+ }
742
+
663
743
  async getTips(): Promise<{ pending: CheckpointNumber; proven: CheckpointNumber }> {
664
744
  const { pending, proven } = await this.rollup.read.getTips();
665
745
  return {
@@ -734,7 +814,7 @@ export class RollupContract {
734
814
  }
735
815
 
736
816
  async getEpochProofPublicInputs(
737
- args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly `0x${string}`[], `0x${string}`],
817
+ args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly ViemHeader[], `0x${string}`],
738
818
  ): Promise<Fr[]> {
739
819
  const result = await this.rollup.read.getEpochProofPublicInputs(args);
740
820
  return result.map(Fr.fromString);
@@ -781,12 +861,10 @@ export class RollupContract {
781
861
  public async canProposeAt(
782
862
  archive: Buffer,
783
863
  account: `0x${string}` | Account,
784
- slotDuration: bigint,
785
- slotOffset: bigint,
786
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
864
+ timestamp: bigint,
865
+ stateOverride: StateOverride = [],
787
866
  ): Promise<{ slot: SlotNumber; checkpointNumber: CheckpointNumber; timeOfNextL1Slot: bigint }> {
788
- const latestBlock = await this.client.getBlock();
789
- const timeOfNextL1Slot = latestBlock.timestamp + slotDuration + slotOffset;
867
+ const timeOfNextL1Slot = timestamp;
790
868
  const who = typeof account === 'string' ? account : account.address;
791
869
 
792
870
  try {
@@ -798,7 +876,7 @@ export class RollupContract {
798
876
  functionName: 'canProposeAtTime',
799
877
  args: [timeOfNextL1Slot, `0x${archive.toString('hex')}`, who],
800
878
  account,
801
- stateOverride: await this.makePendingCheckpointNumberOverride(opts.forcePendingCheckpointNumber),
879
+ stateOverride,
802
880
  });
803
881
 
804
882
  return {
@@ -812,20 +890,32 @@ export class RollupContract {
812
890
  }
813
891
 
814
892
  /**
815
- * Returns a state override that sets the pending checkpoint number to the specified value. Useful for simulations.
816
- * Requires querying the current state of the contract to get the current proven checkpoint number, as they are both
817
- * stored in the same slot. If the argument is undefined, it returns an empty override.
893
+ * Returns a state override that sets the pending and/or proven checkpoint numbers. Useful for simulations.
894
+ * Both values share a single storage slot (pending in the upper 128 bits, proven in the lower 128 bits), so
895
+ * a single combined override is emitted to avoid the second state-diff clobbering the first. The current live
896
+ * value is read once to preserve any half not being overridden. Returns an empty override if neither is set.
897
+ *
898
+ * Throws if the resulting `proven > pending`, which would crash the simulation: `STFLib.canPruneAtTime` calls
899
+ * `getEpochForCheckpoint(proven + 1)` whenever `pending != proven`, and that asserts `_n <= tips.pending`.
818
900
  */
819
- public async makePendingCheckpointNumberOverride(
820
- forcePendingCheckpointNumber: CheckpointNumber | undefined,
821
- ): Promise<StateOverride> {
822
- if (forcePendingCheckpointNumber === undefined) {
901
+ public async makeChainTipsOverride(override: {
902
+ pending?: CheckpointNumber;
903
+ proven?: CheckpointNumber;
904
+ }): Promise<StateOverride> {
905
+ if (override.pending === undefined && override.proven === undefined) {
823
906
  return [];
824
907
  }
825
908
  const slot = RollupContract.stfStorageSlot;
826
909
  const currentValue = await this.client.getStorageAt({ address: this.address, slot });
827
- const currentProvenCheckpointNumber = currentValue ? hexToBigInt(currentValue) & ((1n << 128n) - 1n) : 0n;
828
- const newValue = (BigInt(forcePendingCheckpointNumber) << 128n) | currentProvenCheckpointNumber;
910
+ const currentRaw = currentValue ? hexToBigInt(currentValue) : 0n;
911
+ const currentPending = currentRaw >> 128n;
912
+ const currentProven = currentRaw & ((1n << 128n) - 1n);
913
+ const newPending = override.pending !== undefined ? BigInt(override.pending) : currentPending;
914
+ const newProven = override.proven !== undefined ? BigInt(override.proven) : currentProven;
915
+ if (newProven > newPending) {
916
+ throw new Error(`Invalid chain tips override: proven (${newProven}) > pending (${newPending})`);
917
+ }
918
+ const newValue = (newPending << 128n) | newProven;
829
919
  return [
830
920
  {
831
921
  address: this.address,
@@ -834,6 +924,168 @@ export class RollupContract {
834
924
  ];
835
925
  }
836
926
 
927
+ /**
928
+ * Returns a state override that patches `tempCheckpointLogs[checkpointNumber]` with every field that
929
+ * the L1 contract reads during `propose()` for the checkpoint that builds on top of it (proposer
930
+ * pipelining simulation). Mirrors the writes done by `ProposeLib.addTempCheckpointLog`.
931
+ *
932
+ * `blobCommitmentsHash` and `attestationsHash` are intentionally not exposed here — the propose path
933
+ * never asserts against them, so leaving them at storage zero is harmless.
934
+ */
935
+ public async makeTempCheckpointLogOverride(
936
+ checkpointNumber: CheckpointNumber,
937
+ fields: TempCheckpointLogOverrideFields,
938
+ ): Promise<StateOverride> {
939
+ const constants = await this.getRollupConstants();
940
+ const slotAt = (field: TempCheckpointLogField) =>
941
+ `0x${this.computeTempCheckpointLogStorageSlot(checkpointNumber, field, constants).toString(16).padStart(64, '0')}` as const;
942
+ const word = (v: bigint) => `0x${v.toString(16).padStart(64, '0')}` as const;
943
+
944
+ const stateDiff: { slot: `0x${string}`; value: `0x${string}` }[] = [];
945
+
946
+ if (fields.headerHash) {
947
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.HeaderHash), value: fields.headerHash.toString() });
948
+ }
949
+ if (fields.outHash) {
950
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.OutHash), value: fields.outHash.toString() });
951
+ }
952
+ if (fields.payloadDigest) {
953
+ stateDiff.push({
954
+ slot: slotAt(TempCheckpointLogField.PayloadDigest),
955
+ value: fields.payloadDigest.toString() as `0x${string}`,
956
+ });
957
+ }
958
+ if (fields.slotNumber !== undefined) {
959
+ // CompressedSlot is uint32 on L1 (SafeCast.toUint32 reverts on overflow). Match that behavior here
960
+ // so a malformed override surfaces immediately rather than silently truncating into a wrong slot.
961
+ const slotNumber = BigInt(fields.slotNumber);
962
+ if (slotNumber < 0n || slotNumber > 0xffffffffn) {
963
+ throw new Error(`slotNumber ${slotNumber} does not fit in uint32`);
964
+ }
965
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.SlotNumber), value: word(slotNumber) });
966
+ }
967
+ if (fields.feeHeader) {
968
+ stateDiff.push({
969
+ slot: slotAt(TempCheckpointLogField.FeeHeader),
970
+ value: word(RollupContract.compressFeeHeader(fields.feeHeader)),
971
+ });
972
+ }
973
+
974
+ if (stateDiff.length === 0) {
975
+ return [];
976
+ }
977
+ return [{ address: this.address, stateDiff }];
978
+ }
979
+
980
+ /**
981
+ * Returns a state override that sets archives[checkpointNumber] to the given archive value.
982
+ * Used when simulating a canProposeAtTime call where the local archive differs from L1
983
+ * (e.g. pipelining where the parent checkpoint hasn't landed on L1 yet).
984
+ */
985
+ public makeArchiveOverride(checkpointNumber: CheckpointNumber, archive: Fr): StateOverride {
986
+ const archivesMappingBase = hexToBigInt(RollupContract.stfStorageSlot) + 1n;
987
+ const archiveSlot = hexToBigInt(
988
+ keccak256(
989
+ encodeAbiParameters(
990
+ [{ type: 'uint256' }, { type: 'uint256' }],
991
+ [BigInt(checkpointNumber), archivesMappingBase],
992
+ ),
993
+ ),
994
+ );
995
+ return [
996
+ {
997
+ address: this.address,
998
+ stateDiff: [
999
+ {
1000
+ slot: `0x${archiveSlot.toString(16).padStart(64, '0')}`,
1001
+ value: archive.toString(),
1002
+ },
1003
+ ],
1004
+ },
1005
+ ];
1006
+ }
1007
+
1008
+ /** Merges multiple StateOverride arrays, combining stateDiff entries for the same address. */
1009
+ public static mergeStateOverrides(...overrides: StateOverride[]): StateOverride {
1010
+ type StateDiffEntry = { slot: `0x${string}`; value: `0x${string}` };
1011
+ const byAddress = new Map<string, { address: `0x${string}`; balance?: bigint; stateDiff: StateDiffEntry[] }>();
1012
+ for (const override of overrides) {
1013
+ for (const entry of override) {
1014
+ const key = entry.address.toLowerCase();
1015
+ const existing = byAddress.get(key);
1016
+ if (existing) {
1017
+ existing.stateDiff.push(...(entry.stateDiff ?? []));
1018
+ if (entry.balance !== undefined) {
1019
+ existing.balance = entry.balance;
1020
+ }
1021
+ } else {
1022
+ byAddress.set(key, {
1023
+ address: entry.address,
1024
+ balance: entry.balance,
1025
+ stateDiff: [...(entry.stateDiff ?? [])],
1026
+ });
1027
+ }
1028
+ }
1029
+ }
1030
+ return [...byAddress.values()];
1031
+ }
1032
+
1033
+ /** Compresses a FeeHeader into a uint256 matching FeeHeaderLib.compress() in FeeStructs.sol. */
1034
+ public static compressFeeHeader(feeHeader: FeeHeader): bigint {
1035
+ const MASK_48_BITS = (1n << 48n) - 1n;
1036
+ const MASK_64_BITS = (1n << 64n) - 1n;
1037
+ const MASK_63_BITS = (1n << 63n) - 1n;
1038
+
1039
+ let value = BigInt(feeHeader.manaUsed) & ((1n << 32n) - 1n); // bits [0:31]
1040
+ value |= (feeHeader.excessMana < MASK_48_BITS ? feeHeader.excessMana : MASK_48_BITS) << 32n; // bits [32:79]
1041
+ value |= (BigInt(feeHeader.ethPerFeeAsset) & MASK_48_BITS) << 80n; // bits [80:127]
1042
+ value |= (feeHeader.congestionCost < MASK_64_BITS ? feeHeader.congestionCost : MASK_64_BITS) << 128n; // bits [128:191]
1043
+ value |= (feeHeader.proverCost < MASK_63_BITS ? feeHeader.proverCost : MASK_63_BITS) << 192n; // bits [192:254]
1044
+ value |= 1n << 255n; // preheat flag
1045
+ return value;
1046
+ }
1047
+
1048
+ /** Computes the fee header for a child checkpoint given parent fee header and child data.
1049
+ * Must stay in sync with Solidity FeeLib.sol (computeNewEthPerFeeAsset, clampedAdd). */
1050
+ public static computeChildFeeHeader(
1051
+ parentFeeHeader: FeeHeader,
1052
+ childManaUsed: bigint,
1053
+ feeAssetPriceModifier: bigint,
1054
+ manaTarget: bigint,
1055
+ ): FeeHeader {
1056
+ const MIN_ETH_PER_FEE_ASSET = 100n;
1057
+ const MAX_ETH_PER_FEE_ASSET = 100_000_000_000_000n; // 1e14, matches FeeLib.sol
1058
+
1059
+ // excessMana = clampedAdd(parent.excessMana + parent.manaUsed, -manaTarget)
1060
+ const sum = parentFeeHeader.excessMana + parentFeeHeader.manaUsed;
1061
+ const excessMana = sum > manaTarget ? sum - manaTarget : 0n;
1062
+
1063
+ // ethPerFeeAsset = computeNewEthPerFeeAsset(max(parent.ethPerFeeAsset, MIN), modifier)
1064
+ const parentPrice =
1065
+ parentFeeHeader.ethPerFeeAsset > MIN_ETH_PER_FEE_ASSET ? parentFeeHeader.ethPerFeeAsset : MIN_ETH_PER_FEE_ASSET;
1066
+ let newPrice: bigint;
1067
+ if (feeAssetPriceModifier >= 0n) {
1068
+ newPrice = (parentPrice * (10_000n + feeAssetPriceModifier)) / 10_000n;
1069
+ } else {
1070
+ const absMod = -feeAssetPriceModifier;
1071
+ newPrice = (parentPrice * (10_000n - absMod)) / 10_000n;
1072
+ }
1073
+ if (newPrice < MIN_ETH_PER_FEE_ASSET) {
1074
+ newPrice = MIN_ETH_PER_FEE_ASSET;
1075
+ }
1076
+ if (newPrice > MAX_ETH_PER_FEE_ASSET) {
1077
+ newPrice = MAX_ETH_PER_FEE_ASSET;
1078
+ }
1079
+
1080
+ return {
1081
+ excessMana,
1082
+ manaUsed: childManaUsed,
1083
+ ethPerFeeAsset: newPrice,
1084
+ congestionCost: 0n,
1085
+ proverCost: 0n,
1086
+ };
1087
+ }
1088
+
837
1089
  /** Creates a request to Rollup#invalidateBadAttestation to be simulated or sent */
838
1090
  public buildInvalidateBadAttestationRequest(
839
1091
  checkpointNumber: CheckpointNumber,
@@ -882,8 +1134,8 @@ export class RollupContract {
882
1134
  return this.rollup.read.getHasSubmitted([BigInt(epochNumber), BigInt(numberOfCheckpointsInEpoch), prover]);
883
1135
  }
884
1136
 
885
- getManaMinFeeAt(timestamp: bigint, inFeeAsset: boolean): Promise<bigint> {
886
- return this.rollup.read.getManaMinFeeAt([timestamp, inFeeAsset]);
1137
+ getManaMinFeeAt(timestamp: bigint, inFeeAsset: boolean, stateOverride?: StateOverride): Promise<bigint> {
1138
+ return this.rollup.read.getManaMinFeeAt([timestamp, inFeeAsset], { stateOverride });
887
1139
  }
888
1140
 
889
1141
  async getManaMinFeeComponentsAt(timestamp: bigint, inFeeAsset: boolean): Promise<ManaMinFeeComponents> {
@@ -939,15 +1191,21 @@ export class RollupContract {
939
1191
  return this.rollup.read.getSpecificProverRewardsForEpoch([epoch, prover]);
940
1192
  }
941
1193
 
942
- async getAttesters(): Promise<EthAddress[]> {
943
- const attesterSize = await this.getActiveAttesterCount();
1194
+ async getAttesters(timestamp?: bigint): Promise<EthAddress[]> {
1195
+ // Pin every read to a single L1 block so the attester count and the chunked index reads
1196
+ // observe a consistent set. Without this, the count and each chunk default to `latest` and
1197
+ // can straddle a block boundary (or reorg), yielding an inconsistent or truncated set.
1198
+ const block = await this.client.getBlock();
1199
+ const blockNumber = block.number ?? undefined;
1200
+ const ts = timestamp ?? block.timestamp;
1201
+ const attesterSize = await this.getActiveAttesterCount({ blockNumber });
944
1202
  const gse = new GSEContract(this.client, await this.getGSE());
945
- const ts = (await this.client.getBlock()).timestamp;
946
-
947
1203
  const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
948
1204
  const chunks = chunk(indices, 1000);
949
1205
 
950
- const results = await Promise.all(chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk)));
1206
+ const results = await Promise.all(
1207
+ chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk, { blockNumber })),
1208
+ );
951
1209
  return results.flat().map(addr => EthAddress.fromString(addr));
952
1210
  }
953
1211
 
@@ -1049,7 +1307,7 @@ export class RollupContract {
1049
1307
  }
1050
1308
 
1051
1309
  public listenToCheckpointInvalidated(
1052
- callback: (args: { checkpointNumber: CheckpointNumber }) => unknown,
1310
+ callback: (args: { checkpointNumber: CheckpointNumber; event: Log }) => unknown,
1053
1311
  ): WatchContractEventReturnType {
1054
1312
  return this.rollup.watchEvent.CheckpointInvalidated(
1055
1313
  {},
@@ -1058,7 +1316,7 @@ export class RollupContract {
1058
1316
  for (const log of logs) {
1059
1317
  const args = log.args;
1060
1318
  if (args.checkpointNumber !== undefined) {
1061
- callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber) });
1319
+ callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber), event: log });
1062
1320
  }
1063
1321
  }
1064
1322
  },
@@ -1091,6 +1349,17 @@ export class RollupContract {
1091
1349
  );
1092
1350
  }
1093
1351
 
1352
+ /**
1353
+ * Fetches OwnershipTransferred events emitted on the L1 block this rollup was deployed on.
1354
+ * The Rollup inherits from Ownable and emits this event in its constructor, so the event
1355
+ * is guaranteed to exist on `l1StartBlock` for any correctly deployed rollup. Used as a
1356
+ * probe to detect RPC nodes that prune historical logs.
1357
+ */
1358
+ async getOwnershipTransferredEventsAtDeploy() {
1359
+ const l1StartBlock = await this.getL1StartBlock();
1360
+ return await this.rollup.getEvents.OwnershipTransferred({}, { fromBlock: l1StartBlock, toBlock: l1StartBlock });
1361
+ }
1362
+
1094
1363
  /** Fetches CheckpointProposed events within the given block range. */
1095
1364
  async getCheckpointProposedEvents(fromBlock: bigint, toBlock: bigint): Promise<CheckpointProposedLog[]> {
1096
1365
  const logs = await this.rollup.getEvents.CheckpointProposed({}, { fromBlock, toBlock });
@@ -1123,4 +1392,48 @@ export class RollupContract {
1123
1392
  },
1124
1393
  }));
1125
1394
  }
1395
+
1396
+ /** Packs pending and proven checkpoint numbers into the chain tips storage format. */
1397
+ static packChainTips(pendingCheckpointNumber: bigint, provenCheckpointNumber: bigint): bigint {
1398
+ return (pendingCheckpointNumber << 128n) | (provenCheckpointNumber & ((1n << 128n) - 1n));
1399
+ }
1400
+
1401
+ /** Storage slot for the chain tips (offset 0 within the STF storage struct). */
1402
+ static get chainTipsStorageSlot(): bigint {
1403
+ return BigInt(RollupContract.stfStorageSlot);
1404
+ }
1405
+
1406
+ /**
1407
+ * Computes the storage slot for a field within a tempCheckpointLog entry.
1408
+ * @param checkpointNumber - The checkpoint number
1409
+ * @param field - The field within the CompressedTempCheckpointLog struct
1410
+ */
1411
+ async getTempCheckpointLogStorageSlot(
1412
+ checkpointNumber: CheckpointNumber,
1413
+ field: TempCheckpointLogField,
1414
+ ): Promise<bigint> {
1415
+ const [epochDuration, proofSubmissionEpochs] = await Promise.all([
1416
+ this.getEpochDuration(),
1417
+ this.getProofSubmissionEpochs(),
1418
+ ]);
1419
+ return this.computeTempCheckpointLogStorageSlot(checkpointNumber, field, { epochDuration, proofSubmissionEpochs });
1420
+ }
1421
+
1422
+ private computeTempCheckpointLogStorageSlot(
1423
+ checkpointNumber: CheckpointNumber,
1424
+ field: TempCheckpointLogField,
1425
+ constants: { epochDuration: number; proofSubmissionEpochs: number },
1426
+ ): bigint {
1427
+ const fieldOffset = BigInt(field);
1428
+ const { epochDuration, proofSubmissionEpochs } = constants;
1429
+ const roundaboutSize = BigInt(epochDuration) * (BigInt(proofSubmissionEpochs) + 1n) + 1n;
1430
+ const tempCheckpointLogsBase = BigInt(RollupContract.stfStorageSlot) + 2n;
1431
+ const circularIndex = BigInt(checkpointNumber) % roundaboutSize;
1432
+ const entryBase = BigInt(
1433
+ keccak256(
1434
+ encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [circularIndex, tempCheckpointLogsBase]),
1435
+ ),
1436
+ );
1437
+ return entryBase + fieldOffset;
1438
+ }
1126
1439
  }