@aztec/ethereum 0.0.1-commit.001888fc → 0.0.1-commit.017a351

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