@aztec/ethereum 0.0.1-commit.c2eed6949 → 0.0.1-commit.c52d6e7

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 (145) 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 +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 +8 -2
  22. package/dest/contracts/gse.d.ts.map +1 -1
  23. package/dest/contracts/gse.js +11 -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 +82 -14
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +119 -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 +5408 -25
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +349 -66
  39. package/dest/contracts/slashing_proposer.d.ts +170 -0
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/slashing_proposer.js +748 -0
  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 +28 -17
  45. package/dest/foundry_binary.d.ts +13 -0
  46. package/dest/foundry_binary.d.ts.map +1 -0
  47. package/dest/foundry_binary.js +52 -0
  48. package/dest/l1_artifacts.d.ts +14943 -16270
  49. package/dest/l1_artifacts.d.ts.map +1 -1
  50. package/dest/l1_artifacts.js +9 -24
  51. package/dest/l1_contract_addresses.d.ts +61 -65
  52. package/dest/l1_contract_addresses.d.ts.map +1 -1
  53. package/dest/l1_contract_addresses.js +101 -82
  54. package/dest/l1_reader.d.ts +3 -5
  55. package/dest/l1_reader.d.ts.map +1 -1
  56. package/dest/l1_reader.js +3 -6
  57. package/dest/l1_tx_utils/config.js +3 -3
  58. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
  59. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
  60. package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
  61. package/dest/l1_tx_utils/l1_tx_utils.d.ts +9 -1
  62. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  63. package/dest/l1_tx_utils/l1_tx_utils.js +138 -57
  64. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  65. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  66. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
  67. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  68. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  69. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  70. package/dest/l1_tx_utils/types.d.ts +27 -1
  71. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  72. package/dest/l1_tx_utils/types.js +10 -0
  73. package/dest/publisher_manager.d.ts +18 -6
  74. package/dest/publisher_manager.d.ts.map +1 -1
  75. package/dest/publisher_manager.js +44 -7
  76. package/dest/queries.d.ts +13 -2
  77. package/dest/queries.d.ts.map +1 -1
  78. package/dest/queries.js +69 -4
  79. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  80. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  81. package/dest/test/blob_kzg_warmup.js +64 -0
  82. package/dest/test/chain_monitor.d.ts +34 -2
  83. package/dest/test/chain_monitor.d.ts.map +1 -1
  84. package/dest/test/chain_monitor.js +76 -6
  85. package/dest/test/eth_cheat_codes.d.ts +32 -3
  86. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  87. package/dest/test/eth_cheat_codes.js +83 -37
  88. package/dest/test/index.d.ts +2 -1
  89. package/dest/test/index.d.ts.map +1 -1
  90. package/dest/test/index.js +1 -0
  91. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  92. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  93. package/dest/test/rollup_cheat_codes.js +86 -7
  94. package/dest/test/start_anvil.d.ts +1 -1
  95. package/dest/test/start_anvil.d.ts.map +1 -1
  96. package/dest/test/start_anvil.js +41 -5
  97. package/dest/utils.d.ts +9 -2
  98. package/dest/utils.d.ts.map +1 -1
  99. package/dest/utils.js +34 -6
  100. package/package.json +6 -7
  101. package/src/client.ts +57 -1
  102. package/src/config.ts +82 -16
  103. package/src/contracts/chain_state_override.ts +200 -0
  104. package/src/contracts/empire_base.ts +0 -2
  105. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  106. package/src/contracts/governance.ts +310 -10
  107. package/src/contracts/governance_proposer.ts +56 -13
  108. package/src/contracts/gse.ts +19 -2
  109. package/src/contracts/inbox.ts +13 -5
  110. package/src/contracts/index.ts +2 -2
  111. package/src/contracts/multicall.ts +183 -106
  112. package/src/contracts/outbox.ts +42 -8
  113. package/src/contracts/rollup.ts +389 -63
  114. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +91 -30
  115. package/src/deploy_aztec_l1_contracts.ts +32 -18
  116. package/src/foundry_binary.ts +57 -0
  117. package/src/l1_artifacts.ts +12 -35
  118. package/src/l1_contract_addresses.ts +120 -94
  119. package/src/l1_reader.ts +4 -9
  120. package/src/l1_tx_utils/config.ts +3 -3
  121. package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
  122. package/src/l1_tx_utils/l1_tx_utils.ts +122 -48
  123. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
  124. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  125. package/src/l1_tx_utils/types.ts +32 -0
  126. package/src/publisher_manager.ts +54 -18
  127. package/src/queries.ts +76 -4
  128. package/src/test/blob_kzg_warmup.ts +65 -0
  129. package/src/test/chain_monitor.ts +93 -5
  130. package/src/test/eth_cheat_codes.ts +76 -41
  131. package/src/test/index.ts +1 -0
  132. package/src/test/rollup_cheat_codes.ts +115 -6
  133. package/src/test/start_anvil.ts +38 -6
  134. package/src/utils.ts +30 -6
  135. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  136. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  137. package/dest/contracts/empire_slashing_proposer.js +0 -216
  138. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  139. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  140. package/dest/contracts/tally_slashing_proposer.js +0 -320
  141. package/dest/generated/l1-contracts-defaults.d.ts +0 -30
  142. package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
  143. package/dest/generated/l1-contracts-defaults.js +0 -30
  144. package/src/contracts/empire_slashing_proposer.ts +0 -265
  145. 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,19 @@ 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));
561
+ }
562
+
563
+ /**
564
+ * Number of attesters that were staked at the given (historical) L1 timestamp. This is the count that
565
+ * validator-set sampling uses to decide whether an epoch gets a committee, and the historical counterpart
566
+ * of {@link getActiveAttesterCount} (which reads at the latest L1 block).
567
+ */
568
+ async getAttesterCountAtTime(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<number> {
569
+ const gse = new GSEContract(this.client, await this.getGSE());
570
+ return gse.getAttesterCountAtTime(this.address, timestamp, options);
520
571
  }
521
572
 
522
573
  public async getSlashingProposerAddress() {
@@ -531,8 +582,9 @@ export class RollupContract {
531
582
  return this.rollup.read.getCheckpointReward();
532
583
  }
533
584
 
534
- async getCheckpointNumber(): Promise<CheckpointNumber> {
535
- return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber());
585
+ async getCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
586
+ await checkBlockTag(options?.blockNumber, this.client);
587
+ return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber(options));
536
588
  }
537
589
 
538
590
  async getProvenCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
@@ -540,18 +592,31 @@ export class RollupContract {
540
592
  return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber(options));
541
593
  }
542
594
 
543
- async getSlotNumber(): Promise<SlotNumber> {
544
- return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot());
595
+ async getSlotNumber(options?: { blockNumber?: bigint }): Promise<SlotNumber> {
596
+ await checkBlockTag(options?.blockNumber, this.client);
597
+ return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot(options));
545
598
  }
546
599
 
547
- async getL1FeesAt(timestamp: bigint): Promise<L1FeeData> {
548
- const result = await this.rollup.read.getL1FeesAt([timestamp]);
600
+ async getL1FeesAt(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<L1FeeData> {
601
+ await checkBlockTag(options?.blockNumber, this.client);
602
+ const result = await this.rollup.read.getL1FeesAt([timestamp], options);
549
603
  return {
550
604
  baseFee: result.baseFee,
551
605
  blobFee: result.blobFee,
552
606
  };
553
607
  }
554
608
 
609
+ async getFeeHeader(checkpointNumber: bigint): Promise<FeeHeader> {
610
+ const result = await this.rollup.read.getFeeHeader([checkpointNumber]);
611
+ return {
612
+ excessMana: result.excessMana,
613
+ manaUsed: result.manaUsed,
614
+ ethPerFeeAsset: result.ethPerFeeAsset,
615
+ congestionCost: result.congestionCost,
616
+ proverCost: result.proverCost,
617
+ };
618
+ }
619
+
555
620
  getEthPerFeeAsset(): Promise<bigint> {
556
621
  return this.rollup.read.getEthPerFeeAsset();
557
622
  }
@@ -565,7 +630,7 @@ export class RollupContract {
565
630
  args: [timestamp],
566
631
  })
567
632
  .catch(e => {
568
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
633
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
569
634
  return { result: undefined };
570
635
  }
571
636
  throw e;
@@ -595,7 +660,7 @@ export class RollupContract {
595
660
  args: [],
596
661
  })
597
662
  .catch(e => {
598
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
663
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
599
664
  return { result: undefined };
600
665
  }
601
666
  throw e;
@@ -626,8 +691,9 @@ export class RollupContract {
626
691
  return EthAddress.fromString(result);
627
692
  }
628
693
 
629
- async getCheckpoint(checkpointNumber: CheckpointNumber): Promise<CheckpointLog> {
630
- const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)]);
694
+ async getCheckpoint(checkpointNumber: CheckpointNumber, options?: { blockNumber?: bigint }): Promise<CheckpointLog> {
695
+ await checkBlockTag(options?.blockNumber, this.client);
696
+ const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)], options);
631
697
  return {
632
698
  archive: Fr.fromString(result.archive),
633
699
  headerHash: Buffer32.fromString(result.headerHash),
@@ -660,6 +726,30 @@ export class RollupContract {
660
726
  );
661
727
  }
662
728
 
729
+ /**
730
+ * Returns the effective pending checkpoint, accounting for potential prunes.
731
+ * When a prune can happen, the L1 contract uses the proven checkpoint instead of the pending one.
732
+ * This mirrors the behavior of getEffectivePendingCheckpointNumber in STFLib.sol.
733
+ * @param atTimestamp - The timestamp to evaluate pruneability at. Defaults to the current L1 block timestamp.
734
+ * @param options - Optional L1 block number to pin the queries to.
735
+ */
736
+ getEffectivePendingCheckpoint(atTimestamp?: bigint, options?: { blockNumber?: bigint }) {
737
+ return retry(
738
+ async () => {
739
+ const timestamp = atTimestamp ?? (await this.client.getBlock()).timestamp;
740
+ const canPrune = await this.canPruneAtTime(timestamp, options);
741
+ if (canPrune) {
742
+ const provenCheckpointNumber = await this.getProvenCheckpointNumber(options);
743
+ return await this.getCheckpoint(provenCheckpointNumber, options);
744
+ }
745
+ const pendingCheckpointNumber = await this.getCheckpointNumber(options);
746
+ return await this.getCheckpoint(pendingCheckpointNumber, options);
747
+ },
748
+ 'getting effective pending checkpoint',
749
+ makeBackoff([0.5, 0.5, 0.5]),
750
+ );
751
+ }
752
+
663
753
  async getTips(): Promise<{ pending: CheckpointNumber; proven: CheckpointNumber }> {
664
754
  const { pending, proven } = await this.rollup.read.getTips();
665
755
  return {
@@ -734,7 +824,7 @@ export class RollupContract {
734
824
  }
735
825
 
736
826
  async getEpochProofPublicInputs(
737
- args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly `0x${string}`[], `0x${string}`],
827
+ args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly ViemHeader[], `0x${string}`],
738
828
  ): Promise<Fr[]> {
739
829
  const result = await this.rollup.read.getEpochProofPublicInputs(args);
740
830
  return result.map(Fr.fromString);
@@ -782,7 +872,7 @@ export class RollupContract {
782
872
  archive: Buffer,
783
873
  account: `0x${string}` | Account,
784
874
  timestamp: bigint,
785
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
875
+ stateOverride: StateOverride = [],
786
876
  ): Promise<{ slot: SlotNumber; checkpointNumber: CheckpointNumber; timeOfNextL1Slot: bigint }> {
787
877
  const timeOfNextL1Slot = timestamp;
788
878
  const who = typeof account === 'string' ? account : account.address;
@@ -796,7 +886,7 @@ export class RollupContract {
796
886
  functionName: 'canProposeAtTime',
797
887
  args: [timeOfNextL1Slot, `0x${archive.toString('hex')}`, who],
798
888
  account,
799
- stateOverride: await this.makePendingCheckpointNumberOverride(opts.forcePendingCheckpointNumber),
889
+ stateOverride,
800
890
  });
801
891
 
802
892
  return {
@@ -810,20 +900,32 @@ export class RollupContract {
810
900
  }
811
901
 
812
902
  /**
813
- * Returns a state override that sets the pending checkpoint number to the specified value. Useful for simulations.
814
- * Requires querying the current state of the contract to get the current proven checkpoint number, as they are both
815
- * stored in the same slot. If the argument is undefined, it returns an empty override.
903
+ * Returns a state override that sets the pending and/or proven checkpoint numbers. Useful for simulations.
904
+ * Both values share a single storage slot (pending in the upper 128 bits, proven in the lower 128 bits), so
905
+ * a single combined override is emitted to avoid the second state-diff clobbering the first. The current live
906
+ * value is read once to preserve any half not being overridden. Returns an empty override if neither is set.
907
+ *
908
+ * Throws if the resulting `proven > pending`, which would crash the simulation: `STFLib.canPruneAtTime` calls
909
+ * `getEpochForCheckpoint(proven + 1)` whenever `pending != proven`, and that asserts `_n <= tips.pending`.
816
910
  */
817
- public async makePendingCheckpointNumberOverride(
818
- forcePendingCheckpointNumber: CheckpointNumber | undefined,
819
- ): Promise<StateOverride> {
820
- if (forcePendingCheckpointNumber === undefined) {
911
+ public async makeChainTipsOverride(override: {
912
+ pending?: CheckpointNumber;
913
+ proven?: CheckpointNumber;
914
+ }): Promise<StateOverride> {
915
+ if (override.pending === undefined && override.proven === undefined) {
821
916
  return [];
822
917
  }
823
918
  const slot = RollupContract.stfStorageSlot;
824
919
  const currentValue = await this.client.getStorageAt({ address: this.address, slot });
825
- const currentProvenCheckpointNumber = currentValue ? hexToBigInt(currentValue) & ((1n << 128n) - 1n) : 0n;
826
- const newValue = (BigInt(forcePendingCheckpointNumber) << 128n) | currentProvenCheckpointNumber;
920
+ const currentRaw = currentValue ? hexToBigInt(currentValue) : 0n;
921
+ const currentPending = currentRaw >> 128n;
922
+ const currentProven = currentRaw & ((1n << 128n) - 1n);
923
+ const newPending = override.pending !== undefined ? BigInt(override.pending) : currentPending;
924
+ const newProven = override.proven !== undefined ? BigInt(override.proven) : currentProven;
925
+ if (newProven > newPending) {
926
+ throw new Error(`Invalid chain tips override: proven (${newProven}) > pending (${newPending})`);
927
+ }
928
+ const newValue = (newPending << 128n) | newProven;
827
929
  return [
828
930
  {
829
931
  address: this.address,
@@ -832,6 +934,168 @@ export class RollupContract {
832
934
  ];
833
935
  }
834
936
 
937
+ /**
938
+ * Returns a state override that patches `tempCheckpointLogs[checkpointNumber]` with every field that
939
+ * the L1 contract reads during `propose()` for the checkpoint that builds on top of it (proposer
940
+ * pipelining simulation). Mirrors the writes done by `ProposeLib.addTempCheckpointLog`.
941
+ *
942
+ * `blobCommitmentsHash` and `attestationsHash` are intentionally not exposed here — the propose path
943
+ * never asserts against them, so leaving them at storage zero is harmless.
944
+ */
945
+ public async makeTempCheckpointLogOverride(
946
+ checkpointNumber: CheckpointNumber,
947
+ fields: TempCheckpointLogOverrideFields,
948
+ ): Promise<StateOverride> {
949
+ const constants = await this.getRollupConstants();
950
+ const slotAt = (field: TempCheckpointLogField) =>
951
+ `0x${this.computeTempCheckpointLogStorageSlot(checkpointNumber, field, constants).toString(16).padStart(64, '0')}` as const;
952
+ const word = (v: bigint) => `0x${v.toString(16).padStart(64, '0')}` as const;
953
+
954
+ const stateDiff: { slot: `0x${string}`; value: `0x${string}` }[] = [];
955
+
956
+ if (fields.headerHash) {
957
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.HeaderHash), value: fields.headerHash.toString() });
958
+ }
959
+ if (fields.outHash) {
960
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.OutHash), value: fields.outHash.toString() });
961
+ }
962
+ if (fields.payloadDigest) {
963
+ stateDiff.push({
964
+ slot: slotAt(TempCheckpointLogField.PayloadDigest),
965
+ value: fields.payloadDigest.toString() as `0x${string}`,
966
+ });
967
+ }
968
+ if (fields.slotNumber !== undefined) {
969
+ // CompressedSlot is uint32 on L1 (SafeCast.toUint32 reverts on overflow). Match that behavior here
970
+ // so a malformed override surfaces immediately rather than silently truncating into a wrong slot.
971
+ const slotNumber = BigInt(fields.slotNumber);
972
+ if (slotNumber < 0n || slotNumber > 0xffffffffn) {
973
+ throw new Error(`slotNumber ${slotNumber} does not fit in uint32`);
974
+ }
975
+ stateDiff.push({ slot: slotAt(TempCheckpointLogField.SlotNumber), value: word(slotNumber) });
976
+ }
977
+ if (fields.feeHeader) {
978
+ stateDiff.push({
979
+ slot: slotAt(TempCheckpointLogField.FeeHeader),
980
+ value: word(RollupContract.compressFeeHeader(fields.feeHeader)),
981
+ });
982
+ }
983
+
984
+ if (stateDiff.length === 0) {
985
+ return [];
986
+ }
987
+ return [{ address: this.address, stateDiff }];
988
+ }
989
+
990
+ /**
991
+ * Returns a state override that sets archives[checkpointNumber] to the given archive value.
992
+ * Used when simulating a canProposeAtTime call where the local archive differs from L1
993
+ * (e.g. pipelining where the parent checkpoint hasn't landed on L1 yet).
994
+ */
995
+ public makeArchiveOverride(checkpointNumber: CheckpointNumber, archive: Fr): StateOverride {
996
+ const archivesMappingBase = hexToBigInt(RollupContract.stfStorageSlot) + 1n;
997
+ const archiveSlot = hexToBigInt(
998
+ keccak256(
999
+ encodeAbiParameters(
1000
+ [{ type: 'uint256' }, { type: 'uint256' }],
1001
+ [BigInt(checkpointNumber), archivesMappingBase],
1002
+ ),
1003
+ ),
1004
+ );
1005
+ return [
1006
+ {
1007
+ address: this.address,
1008
+ stateDiff: [
1009
+ {
1010
+ slot: `0x${archiveSlot.toString(16).padStart(64, '0')}`,
1011
+ value: archive.toString(),
1012
+ },
1013
+ ],
1014
+ },
1015
+ ];
1016
+ }
1017
+
1018
+ /** Merges multiple StateOverride arrays, combining stateDiff entries for the same address. */
1019
+ public static mergeStateOverrides(...overrides: StateOverride[]): StateOverride {
1020
+ type StateDiffEntry = { slot: `0x${string}`; value: `0x${string}` };
1021
+ const byAddress = new Map<string, { address: `0x${string}`; balance?: bigint; stateDiff: StateDiffEntry[] }>();
1022
+ for (const override of overrides) {
1023
+ for (const entry of override) {
1024
+ const key = entry.address.toLowerCase();
1025
+ const existing = byAddress.get(key);
1026
+ if (existing) {
1027
+ existing.stateDiff.push(...(entry.stateDiff ?? []));
1028
+ if (entry.balance !== undefined) {
1029
+ existing.balance = entry.balance;
1030
+ }
1031
+ } else {
1032
+ byAddress.set(key, {
1033
+ address: entry.address,
1034
+ balance: entry.balance,
1035
+ stateDiff: [...(entry.stateDiff ?? [])],
1036
+ });
1037
+ }
1038
+ }
1039
+ }
1040
+ return [...byAddress.values()];
1041
+ }
1042
+
1043
+ /** Compresses a FeeHeader into a uint256 matching FeeHeaderLib.compress() in FeeStructs.sol. */
1044
+ public static compressFeeHeader(feeHeader: FeeHeader): bigint {
1045
+ const MASK_48_BITS = (1n << 48n) - 1n;
1046
+ const MASK_64_BITS = (1n << 64n) - 1n;
1047
+ const MASK_63_BITS = (1n << 63n) - 1n;
1048
+
1049
+ let value = BigInt(feeHeader.manaUsed) & ((1n << 32n) - 1n); // bits [0:31]
1050
+ value |= (feeHeader.excessMana < MASK_48_BITS ? feeHeader.excessMana : MASK_48_BITS) << 32n; // bits [32:79]
1051
+ value |= (BigInt(feeHeader.ethPerFeeAsset) & MASK_48_BITS) << 80n; // bits [80:127]
1052
+ value |= (feeHeader.congestionCost < MASK_64_BITS ? feeHeader.congestionCost : MASK_64_BITS) << 128n; // bits [128:191]
1053
+ value |= (feeHeader.proverCost < MASK_63_BITS ? feeHeader.proverCost : MASK_63_BITS) << 192n; // bits [192:254]
1054
+ value |= 1n << 255n; // preheat flag
1055
+ return value;
1056
+ }
1057
+
1058
+ /** Computes the fee header for a child checkpoint given parent fee header and child data.
1059
+ * Must stay in sync with Solidity FeeLib.sol (computeNewEthPerFeeAsset, clampedAdd). */
1060
+ public static computeChildFeeHeader(
1061
+ parentFeeHeader: FeeHeader,
1062
+ childManaUsed: bigint,
1063
+ feeAssetPriceModifier: bigint,
1064
+ manaTarget: bigint,
1065
+ ): FeeHeader {
1066
+ const MIN_ETH_PER_FEE_ASSET = 100n;
1067
+ const MAX_ETH_PER_FEE_ASSET = 100_000_000_000_000n; // 1e14, matches FeeLib.sol
1068
+
1069
+ // excessMana = clampedAdd(parent.excessMana + parent.manaUsed, -manaTarget)
1070
+ const sum = parentFeeHeader.excessMana + parentFeeHeader.manaUsed;
1071
+ const excessMana = sum > manaTarget ? sum - manaTarget : 0n;
1072
+
1073
+ // ethPerFeeAsset = computeNewEthPerFeeAsset(max(parent.ethPerFeeAsset, MIN), modifier)
1074
+ const parentPrice =
1075
+ parentFeeHeader.ethPerFeeAsset > MIN_ETH_PER_FEE_ASSET ? parentFeeHeader.ethPerFeeAsset : MIN_ETH_PER_FEE_ASSET;
1076
+ let newPrice: bigint;
1077
+ if (feeAssetPriceModifier >= 0n) {
1078
+ newPrice = (parentPrice * (10_000n + feeAssetPriceModifier)) / 10_000n;
1079
+ } else {
1080
+ const absMod = -feeAssetPriceModifier;
1081
+ newPrice = (parentPrice * (10_000n - absMod)) / 10_000n;
1082
+ }
1083
+ if (newPrice < MIN_ETH_PER_FEE_ASSET) {
1084
+ newPrice = MIN_ETH_PER_FEE_ASSET;
1085
+ }
1086
+ if (newPrice > MAX_ETH_PER_FEE_ASSET) {
1087
+ newPrice = MAX_ETH_PER_FEE_ASSET;
1088
+ }
1089
+
1090
+ return {
1091
+ excessMana,
1092
+ manaUsed: childManaUsed,
1093
+ ethPerFeeAsset: newPrice,
1094
+ congestionCost: 0n,
1095
+ proverCost: 0n,
1096
+ };
1097
+ }
1098
+
835
1099
  /** Creates a request to Rollup#invalidateBadAttestation to be simulated or sent */
836
1100
  public buildInvalidateBadAttestationRequest(
837
1101
  checkpointNumber: CheckpointNumber,
@@ -880,8 +1144,8 @@ export class RollupContract {
880
1144
  return this.rollup.read.getHasSubmitted([BigInt(epochNumber), BigInt(numberOfCheckpointsInEpoch), prover]);
881
1145
  }
882
1146
 
883
- getManaMinFeeAt(timestamp: bigint, inFeeAsset: boolean): Promise<bigint> {
884
- return this.rollup.read.getManaMinFeeAt([timestamp, inFeeAsset]);
1147
+ getManaMinFeeAt(timestamp: bigint, inFeeAsset: boolean, stateOverride?: StateOverride): Promise<bigint> {
1148
+ return this.rollup.read.getManaMinFeeAt([timestamp, inFeeAsset], { stateOverride });
885
1149
  }
886
1150
 
887
1151
  async getManaMinFeeComponentsAt(timestamp: bigint, inFeeAsset: boolean): Promise<ManaMinFeeComponents> {
@@ -938,13 +1202,20 @@ export class RollupContract {
938
1202
  }
939
1203
 
940
1204
  async getAttesters(timestamp?: bigint): Promise<EthAddress[]> {
941
- const attesterSize = await this.getActiveAttesterCount();
1205
+ // Pin every read to a single L1 block so the attester count and the chunked index reads
1206
+ // observe a consistent set. Without this, the count and each chunk default to `latest` and
1207
+ // can straddle a block boundary (or reorg), yielding an inconsistent or truncated set.
1208
+ const block = await this.client.getBlock();
1209
+ const blockNumber = block.number ?? undefined;
1210
+ const ts = timestamp ?? block.timestamp;
1211
+ const attesterSize = await this.getActiveAttesterCount({ blockNumber });
942
1212
  const gse = new GSEContract(this.client, await this.getGSE());
943
- const ts = timestamp ?? (await this.client.getBlock()).timestamp;
944
1213
  const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
945
1214
  const chunks = chunk(indices, 1000);
946
1215
 
947
- const results = await Promise.all(chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk)));
1216
+ const results = await Promise.all(
1217
+ chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk, { blockNumber })),
1218
+ );
948
1219
  return results.flat().map(addr => EthAddress.fromString(addr));
949
1220
  }
950
1221
 
@@ -1046,7 +1317,7 @@ export class RollupContract {
1046
1317
  }
1047
1318
 
1048
1319
  public listenToCheckpointInvalidated(
1049
- callback: (args: { checkpointNumber: CheckpointNumber }) => unknown,
1320
+ callback: (args: { checkpointNumber: CheckpointNumber; event: Log }) => unknown,
1050
1321
  ): WatchContractEventReturnType {
1051
1322
  return this.rollup.watchEvent.CheckpointInvalidated(
1052
1323
  {},
@@ -1055,7 +1326,7 @@ export class RollupContract {
1055
1326
  for (const log of logs) {
1056
1327
  const args = log.args;
1057
1328
  if (args.checkpointNumber !== undefined) {
1058
- callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber) });
1329
+ callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber), event: log });
1059
1330
  }
1060
1331
  }
1061
1332
  },
@@ -1088,6 +1359,17 @@ export class RollupContract {
1088
1359
  );
1089
1360
  }
1090
1361
 
1362
+ /**
1363
+ * Fetches OwnershipTransferred events emitted on the L1 block this rollup was deployed on.
1364
+ * The Rollup inherits from Ownable and emits this event in its constructor, so the event
1365
+ * is guaranteed to exist on `l1StartBlock` for any correctly deployed rollup. Used as a
1366
+ * probe to detect RPC nodes that prune historical logs.
1367
+ */
1368
+ async getOwnershipTransferredEventsAtDeploy() {
1369
+ const l1StartBlock = await this.getL1StartBlock();
1370
+ return await this.rollup.getEvents.OwnershipTransferred({}, { fromBlock: l1StartBlock, toBlock: l1StartBlock });
1371
+ }
1372
+
1091
1373
  /** Fetches CheckpointProposed events within the given block range. */
1092
1374
  async getCheckpointProposedEvents(fromBlock: bigint, toBlock: bigint): Promise<CheckpointProposedLog[]> {
1093
1375
  const logs = await this.rollup.getEvents.CheckpointProposed({}, { fromBlock, toBlock });
@@ -1120,4 +1402,48 @@ export class RollupContract {
1120
1402
  },
1121
1403
  }));
1122
1404
  }
1405
+
1406
+ /** Packs pending and proven checkpoint numbers into the chain tips storage format. */
1407
+ static packChainTips(pendingCheckpointNumber: bigint, provenCheckpointNumber: bigint): bigint {
1408
+ return (pendingCheckpointNumber << 128n) | (provenCheckpointNumber & ((1n << 128n) - 1n));
1409
+ }
1410
+
1411
+ /** Storage slot for the chain tips (offset 0 within the STF storage struct). */
1412
+ static get chainTipsStorageSlot(): bigint {
1413
+ return BigInt(RollupContract.stfStorageSlot);
1414
+ }
1415
+
1416
+ /**
1417
+ * Computes the storage slot for a field within a tempCheckpointLog entry.
1418
+ * @param checkpointNumber - The checkpoint number
1419
+ * @param field - The field within the CompressedTempCheckpointLog struct
1420
+ */
1421
+ async getTempCheckpointLogStorageSlot(
1422
+ checkpointNumber: CheckpointNumber,
1423
+ field: TempCheckpointLogField,
1424
+ ): Promise<bigint> {
1425
+ const [epochDuration, proofSubmissionEpochs] = await Promise.all([
1426
+ this.getEpochDuration(),
1427
+ this.getProofSubmissionEpochs(),
1428
+ ]);
1429
+ return this.computeTempCheckpointLogStorageSlot(checkpointNumber, field, { epochDuration, proofSubmissionEpochs });
1430
+ }
1431
+
1432
+ private computeTempCheckpointLogStorageSlot(
1433
+ checkpointNumber: CheckpointNumber,
1434
+ field: TempCheckpointLogField,
1435
+ constants: { epochDuration: number; proofSubmissionEpochs: number },
1436
+ ): bigint {
1437
+ const fieldOffset = BigInt(field);
1438
+ const { epochDuration, proofSubmissionEpochs } = constants;
1439
+ const roundaboutSize = BigInt(epochDuration) * (BigInt(proofSubmissionEpochs) + 1n) + 1n;
1440
+ const tempCheckpointLogsBase = BigInt(RollupContract.stfStorageSlot) + 2n;
1441
+ const circularIndex = BigInt(checkpointNumber) % roundaboutSize;
1442
+ const entryBase = BigInt(
1443
+ keccak256(
1444
+ encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [circularIndex, tempCheckpointLogsBase]),
1445
+ ),
1446
+ );
1447
+ return entryBase + fieldOffset;
1448
+ }
1123
1449
  }