@aztec/ethereum 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a

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 (72) hide show
  1. package/dest/client.d.ts +11 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +35 -3
  4. package/dest/contracts/chain_state_override.d.ts +47 -7
  5. package/dest/contracts/chain_state_override.d.ts.map +1 -1
  6. package/dest/contracts/chain_state_override.js +57 -20
  7. package/dest/contracts/empire_base.d.ts +1 -3
  8. package/dest/contracts/empire_base.d.ts.map +1 -1
  9. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  10. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  11. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  12. package/dest/contracts/governance.d.ts +108 -33
  13. package/dest/contracts/governance.d.ts.map +1 -1
  14. package/dest/contracts/governance.js +193 -12
  15. package/dest/contracts/governance_proposer.d.ts +33 -3
  16. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  17. package/dest/contracts/governance_proposer.js +51 -9
  18. package/dest/contracts/inbox.js +1 -1
  19. package/dest/contracts/multicall.d.ts +79 -13
  20. package/dest/contracts/multicall.d.ts.map +1 -1
  21. package/dest/contracts/multicall.js +117 -95
  22. package/dest/contracts/outbox.d.ts +27 -4
  23. package/dest/contracts/outbox.d.ts.map +1 -1
  24. package/dest/contracts/outbox.js +34 -7
  25. package/dest/contracts/rollup.d.ts +327 -57
  26. package/dest/contracts/rollup.d.ts.map +1 -1
  27. package/dest/contracts/rollup.js +102 -40
  28. package/dest/l1_artifacts.d.ts +5108 -155
  29. package/dest/l1_artifacts.d.ts.map +1 -1
  30. package/dest/l1_contract_addresses.d.ts +61 -61
  31. package/dest/l1_contract_addresses.d.ts.map +1 -1
  32. package/dest/l1_contract_addresses.js +101 -76
  33. package/dest/l1_reader.d.ts +3 -5
  34. package/dest/l1_reader.d.ts.map +1 -1
  35. package/dest/l1_reader.js +3 -6
  36. package/dest/l1_tx_utils/l1_tx_utils.d.ts +7 -1
  37. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  38. package/dest/l1_tx_utils/l1_tx_utils.js +21 -10
  39. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  40. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  41. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
  42. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  43. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  44. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  45. package/dest/test/eth_cheat_codes.d.ts +7 -1
  46. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  47. package/dest/test/eth_cheat_codes.js +11 -0
  48. package/dest/test/rollup_cheat_codes.d.ts +27 -3
  49. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  50. package/dest/test/rollup_cheat_codes.js +50 -6
  51. package/dest/utils.d.ts +9 -2
  52. package/dest/utils.d.ts.map +1 -1
  53. package/dest/utils.js +34 -6
  54. package/package.json +6 -6
  55. package/src/client.ts +39 -1
  56. package/src/contracts/chain_state_override.ts +77 -24
  57. package/src/contracts/empire_base.ts +0 -2
  58. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  59. package/src/contracts/governance.ts +266 -9
  60. package/src/contracts/governance_proposer.ts +52 -5
  61. package/src/contracts/inbox.ts +1 -1
  62. package/src/contracts/multicall.ts +179 -105
  63. package/src/contracts/outbox.ts +42 -8
  64. package/src/contracts/rollup.ts +124 -42
  65. package/src/l1_contract_addresses.ts +120 -87
  66. package/src/l1_reader.ts +4 -9
  67. package/src/l1_tx_utils/l1_tx_utils.ts +27 -14
  68. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
  69. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  70. package/src/test/eth_cheat_codes.ts +10 -0
  71. package/src/test/rollup_cheat_codes.ts +53 -5
  72. package/src/utils.ts +30 -6
@@ -1,16 +1,37 @@
1
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
2
- import { TimeoutError } from '@aztec/foundation/error';
1
+ import { EthAddress } from '@aztec/foundation/eth-address';
3
2
  import type { Logger } from '@aztec/foundation/log';
4
3
 
5
- import { type Address, type EncodeFunctionDataParameters, type Hex, encodeFunctionData, multicall3Abi } from 'viem';
4
+ import {
5
+ type Abi,
6
+ type Address,
7
+ type BlockOverrides,
8
+ type Hex,
9
+ type RequiredBy,
10
+ type StateOverride,
11
+ type TransactionReceipt,
12
+ decodeFunctionResult,
13
+ encodeFunctionData,
14
+ multicall3Abi,
15
+ } from 'viem';
6
16
 
7
17
  import type { L1BlobInputs, L1TxConfig, L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
8
18
  import type { ExtendedViemWalletClient } from '../types.js';
9
- import { FormattedViemError, formatViemError } from '../utils.js';
10
- import { RollupContract } from './rollup.js';
19
+ import { tryDecodeRevertReason } from '../utils.js';
11
20
 
12
21
  export const MULTI_CALL_3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11' as const;
13
22
 
23
+ /**
24
+ * Thrown by `Multicall3.forward` when the forwarder transaction lands but the receipt reports a
25
+ * reverted status. This is not expected (aggregate3 uses allowFailure: true), so callers should
26
+ * treat it as a fatal on-chain failure rather than retrying on a different publisher.
27
+ */
28
+ export class MulticallForwarderRevertedError extends Error {
29
+ constructor(public readonly receipt: TransactionReceipt) {
30
+ super(`Multicall3 forwarder tx reverted: ${receipt.transactionHash}`);
31
+ this.name = 'MulticallForwarderRevertedError';
32
+ }
33
+ }
34
+
14
35
  /** ABI fragment for aggregate3Value — not included in viem's multicall3Abi. */
15
36
  export const aggregate3ValueAbi = [
16
37
  {
@@ -44,116 +65,169 @@ export const aggregate3ValueAbi = [
44
65
  },
45
66
  ] as const;
46
67
 
68
+ /** A single call to embed inside an aggregate3 simulation. The abi is used to decode revert reasons. */
69
+ export type SimulateAggregate3Request = {
70
+ to: Address;
71
+ data: Hex;
72
+ /** Optional ABI used to decode the revert reason if this entry reverts. */
73
+ abi?: Abi;
74
+ };
75
+
76
+ export type SimulateAggregate3EntryResult = {
77
+ success: boolean;
78
+ /** Decoded revert reason text when `success === false` and a request abi was provided. */
79
+ revertReason?: string;
80
+ /** Raw return data hex. `'0x'` for successful entries with void return. */
81
+ returnData: Hex;
82
+ };
83
+
84
+ /**
85
+ * Outcome of a bundle simulation.
86
+ * - `decoded`: eth_simulateV1 ran and produced a per-entry Result[]. Use `entries` for filtering.
87
+ * - `fallback`: the node does not support eth_simulateV1; `fallbackGasEstimate` was returned and no
88
+ * per-entry info is available. Caller should send the bundle as-is with a conservative gas cap.
89
+ */
90
+ export type SimulateAggregate3Result =
91
+ | { kind: 'decoded'; entries: SimulateAggregate3EntryResult[]; gasUsed: bigint }
92
+ | { kind: 'fallback'; gasUsed: bigint };
93
+
94
+ export type SimulateAggregate3Options = {
95
+ blockOverrides?: BlockOverrides<bigint, number>;
96
+ stateOverrides?: StateOverride;
97
+ /**
98
+ * If set, append a state override that fakes the sender's balance during the simulation so a
99
+ * low or zero balance does not cause the simulate to fail with insufficient funds. The fake
100
+ * balance is applied to `l1TxUtils.getSenderAddress()`.
101
+ */
102
+ fakeSenderBalance?: bigint;
103
+ /** Gas cap to pass on the simulate call itself (defaults to viem's behavior). */
104
+ gas?: bigint;
105
+ /** When eth_simulateV1 is unavailable, fall back to this gas estimate instead of throwing. */
106
+ fallbackGasEstimate?: bigint;
107
+ };
108
+
47
109
  export class Multicall3 {
48
- static async forward(
110
+ /**
111
+ * Returns true iff Multicall3 bytecode is deployed at MULTI_CALL_3_ADDRESS. An empty result from
112
+ * a non-existent contract would otherwise silently validate any bundle that uses Multicall3.
113
+ */
114
+ static async hasCode(l1TxUtils: L1TxUtils): Promise<boolean> {
115
+ const code = await l1TxUtils.getCode(EthAddress.fromString(MULTI_CALL_3_ADDRESS));
116
+ return !!code && code !== '0x';
117
+ }
118
+
119
+ /**
120
+ * Simulates an aggregate3 call composed of the given requests via eth_simulateV1 and decodes the
121
+ * per-entry Result[]. Entries that revert are returned with a decoded revertReason (if the request
122
+ * provided an abi).
123
+ *
124
+ * Use this to pre-validate a bundle before sending it through `Multicall3.forward`. The caller can
125
+ * drop reverted entries from the bundle and re-simulate with the reduced list to get an accurate
126
+ * `gasUsed`.
127
+ */
128
+ static async simulateAggregate3(
129
+ requests: SimulateAggregate3Request[],
130
+ l1TxUtils: L1TxUtils,
131
+ opts: SimulateAggregate3Options = {},
132
+ ): Promise<SimulateAggregate3Result> {
133
+ const calldata = encodeFunctionData({
134
+ abi: multicall3Abi,
135
+ functionName: 'aggregate3',
136
+ args: [
137
+ requests.map(r => ({
138
+ target: r.to,
139
+ callData: r.data,
140
+ allowFailure: true,
141
+ })),
142
+ ],
143
+ });
144
+
145
+ const stateOverrides: StateOverride = [...(opts.stateOverrides ?? [])];
146
+ if (opts.fakeSenderBalance !== undefined) {
147
+ stateOverrides.push({
148
+ address: l1TxUtils.getSenderAddress().toString(),
149
+ balance: opts.fakeSenderBalance,
150
+ });
151
+ }
152
+
153
+ const simResult = await l1TxUtils.simulate(
154
+ { to: MULTI_CALL_3_ADDRESS, data: calldata, gas: opts.gas },
155
+ opts.blockOverrides,
156
+ stateOverrides,
157
+ multicall3Abi,
158
+ { fallbackGasEstimate: opts.fallbackGasEstimate },
159
+ );
160
+
161
+ if (simResult.result === '0x') {
162
+ return { kind: 'fallback', gasUsed: simResult.gasUsed };
163
+ }
164
+
165
+ const decoded = decodeFunctionResult({
166
+ abi: multicall3Abi,
167
+ functionName: 'aggregate3',
168
+ data: simResult.result,
169
+ }) as readonly { success: boolean; returnData: `0x${string}` }[];
170
+
171
+ const entries: SimulateAggregate3EntryResult[] = decoded.map((entry, i) => {
172
+ if (entry.success) {
173
+ return { success: true, returnData: entry.returnData };
174
+ }
175
+ const abi = requests[i].abi;
176
+ const revertReason = abi ? tryDecodeRevertReason(entry.returnData, abi) : undefined;
177
+ return { success: false, returnData: entry.returnData, revertReason };
178
+ });
179
+
180
+ return { kind: 'decoded', entries, gasUsed: simResult.gasUsed };
181
+ }
182
+
183
+ /**
184
+ * Sends a batch of requests through aggregate3. Individual calls may fail (allowFailure: true),
185
+ * but the top-level multicall is expected to land successfully. Throws if the send fails or if
186
+ * the receipt reports a reverted status.
187
+ */
188
+ static async forward<TOptGasLimitRequired extends boolean>(
49
189
  requests: L1TxRequest[],
50
190
  l1TxUtils: L1TxUtils,
51
- gasConfig: L1TxConfig | undefined,
191
+ gasConfig: TOptGasLimitRequired extends true ? RequiredBy<L1TxConfig, 'gasLimit'> : L1TxConfig | undefined,
52
192
  blobConfig: L1BlobInputs | undefined,
53
- rollupAddress: Hex,
54
- logger: Logger,
55
- opts: { revertOnFailure?: boolean } = {},
193
+ opts: { gasLimitRequired?: TOptGasLimitRequired } = {},
56
194
  ) {
57
- requests = requests.filter(request => request.to !== null);
58
- const args = requests.map(r => ({
59
- target: r.to!,
60
- callData: r.data!,
61
- allowFailure: !opts.revertOnFailure,
62
- }));
63
- const forwarderFunctionData: Required<EncodeFunctionDataParameters<typeof multicall3Abi, 'aggregate3'>> = {
195
+ if (opts.gasLimitRequired && !gasConfig?.gasLimit) {
196
+ throw new Error('Multicall gasLimit is required when gasLimitRequired is true');
197
+ }
198
+
199
+ const args = requests
200
+ .filter(request => request.to !== null)
201
+ .map(r => ({
202
+ target: r.to!,
203
+ callData: r.data!,
204
+ allowFailure: true,
205
+ }));
206
+ const encodedForwarderData = encodeFunctionData({
64
207
  abi: multicall3Abi,
65
208
  functionName: 'aggregate3',
66
209
  args: [args],
67
- };
68
-
69
- const encodedForwarderData = encodeFunctionData(forwarderFunctionData);
70
- try {
71
- const { receipt, state } = await l1TxUtils.sendAndMonitorTransaction(
72
- {
73
- to: MULTI_CALL_3_ADDRESS,
74
- data: encodedForwarderData,
75
- abi: multicall3Abi,
76
- },
77
- gasConfig,
78
- blobConfig,
79
- );
80
-
81
- if (receipt.status === 'success') {
82
- const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
83
- return { receipt, stats };
84
- } else {
85
- logger.error('Forwarder transaction failed', undefined, { receipt });
86
-
87
- const args = {
88
- ...forwarderFunctionData,
89
- address: MULTI_CALL_3_ADDRESS,
90
- };
91
-
92
- let errorMsg: string | undefined;
93
-
94
- if (blobConfig) {
95
- const maxFeePerBlobGas = blobConfig.maxFeePerBlobGas ?? state.gasPrice.maxFeePerBlobGas;
96
- if (maxFeePerBlobGas === undefined) {
97
- errorMsg = 'maxFeePerBlobGas is required to get the error message';
98
- } else {
99
- logger.debug('Trying to get error from reverted tx with blob config');
100
- errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(
101
- encodedForwarderData,
102
- args,
103
- {
104
- blobs: blobConfig.blobs,
105
- kzg: blobConfig.kzg,
106
- maxFeePerBlobGas,
107
- },
108
- [
109
- {
110
- address: rollupAddress,
111
- stateDiff: [
112
- {
113
- slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
114
- value: toPaddedHex(0n, true),
115
- },
116
- ],
117
- },
118
- ],
119
- );
120
- }
121
- } else {
122
- logger.debug('Trying to get error from reverted tx without blob config');
123
- errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(encodedForwarderData, args, undefined, []);
124
- }
125
-
126
- return { receipt, errorMsg };
127
- }
128
- } catch (err) {
129
- if (err instanceof TimeoutError) {
130
- throw err;
131
- }
210
+ });
132
211
 
133
- for (const request of requests) {
134
- logger.debug('Simulating request', { request });
135
- const result = await l1TxUtils
136
- .simulate(request, undefined, [
137
- {
138
- address: rollupAddress,
139
- stateDiff: [
140
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
141
- ],
142
- },
143
- ])
144
- .catch(err => formatViemError(err, request.abi));
145
- if (result instanceof FormattedViemError) {
146
- logger.error('Found error in simulation', result, {
147
- to: request.to ?? 'null',
148
- data: request.data,
149
- });
150
-
151
- return result;
152
- }
153
- }
154
- logger.warn('Failed to get error from reverted tx', { err });
155
- throw err;
212
+ const { receipt } = await l1TxUtils.sendAndMonitorTransaction(
213
+ {
214
+ to: MULTI_CALL_3_ADDRESS,
215
+ data: encodedForwarderData,
216
+ abi: multicall3Abi,
217
+ },
218
+ gasConfig,
219
+ blobConfig,
220
+ );
221
+
222
+ // This shouldn't happen. Any failure in individual calls is swallowed by forward since we set
223
+ // allowFailure to true for all calls, so a reverted status here would indicate a problem with
224
+ // the Multicall3 contract itself or the forwarder transaction (such as an out-of-gas).
225
+ if (receipt.status !== 'success') {
226
+ throw new MulticallForwarderRevertedError(receipt);
156
227
  }
228
+
229
+ const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
230
+ return { receipt, stats, multicallData: encodedForwarderData };
157
231
  }
158
232
 
159
233
  /** Batch multiple value transfers into a single aggregate3Value call on Multicall3. */
@@ -1,4 +1,5 @@
1
1
  import type { EpochNumber } from '@aztec/foundation/branded-types';
2
+ import { Fr } from '@aztec/foundation/curves/bn254';
2
3
  import { EthAddress } from '@aztec/foundation/eth-address';
3
4
  import { OutboxAbi } from '@aztec/l1-artifacts/OutboxAbi';
4
5
 
@@ -38,12 +39,24 @@ export class OutboxContract {
38
39
 
39
40
  static getFromConfig(config: L1ReaderConfig) {
40
41
  const client = getPublicClient(config);
41
- const address = config.l1Contracts.outboxAddress.toString();
42
+ const address = config.outboxAddress.toString();
42
43
  return new OutboxContract(client, address);
43
44
  }
44
45
 
45
- static getEpochRootStorageSlot(epoch: EpochNumber) {
46
- return hexToBigInt(keccak256(encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [BigInt(epoch), 0n])));
46
+ /**
47
+ * Storage slot of `epochs[epoch].roots[numCheckpointsInEpoch - 1]` in the Outbox contract.
48
+ *
49
+ * The Outbox lays out storage as `mapping(Epoch => EpochData) internal epochs` at base slot 0
50
+ * (ROLLUP and VERSION are immutable and do not occupy slots). For a mapping at slot `b`, the
51
+ * value for key `k` begins at `keccak256(abi.encode(k, b))`. `EpochData` starts with a fixed-
52
+ * size `bytes32[MAX_CHECKPOINTS_PER_EPOCH] roots` array, so `roots[i]` sits at the EpochData's
53
+ * base slot plus `i`. `numCheckpointsInEpoch` is 1-indexed (matches `Outbox.insert`/`consume`).
54
+ */
55
+ static getEpochRootStorageSlot(epoch: EpochNumber, numCheckpointsInEpoch: number) {
56
+ const epochDataSlot = hexToBigInt(
57
+ keccak256(encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [BigInt(epoch), 0n])),
58
+ );
59
+ return epochDataSlot + BigInt(numCheckpointsInEpoch - 1);
47
60
  }
48
61
 
49
62
  constructor(
@@ -68,24 +81,45 @@ export class OutboxContract {
68
81
  return this.outbox.read.hasMessageBeenConsumedAtEpoch([BigInt(epoch), leafId]);
69
82
  }
70
83
 
71
- public getRootData(epoch: EpochNumber) {
72
- return this.outbox.read.getRootData([BigInt(epoch)]);
84
+ public getRootData(epoch: EpochNumber, numCheckpointsInEpoch: number) {
85
+ return this.outbox.read.getRootData([BigInt(epoch), BigInt(numCheckpointsInEpoch)]);
73
86
  }
74
87
 
75
- public consume(message: ViemL2ToL1Msg, epoch: EpochNumber, leafIndex: bigint, path: Hex[]) {
88
+ /**
89
+ * Returns every root stored for `epoch`. Slot `i` of the returned array holds the root inserted
90
+ * for `numCheckpointsInEpoch = i + 1`, or `Fr.ZERO` if no proof of that depth has been inserted
91
+ * yet. The array length is always `MAX_CHECKPOINTS_PER_EPOCH`.
92
+ *
93
+ * @param opts - Optional viem read options. Pass `blockNumber` to pin the read to a specific L1
94
+ * block, which is important for callers that want a consistent snapshot across multiple reads
95
+ * (e.g. the archiver's outbox-trees resolver, which pins to the node's synced L1 block).
96
+ */
97
+ public async getRoots(epoch: EpochNumber, opts?: { blockNumber?: bigint }): Promise<Fr[]> {
98
+ const raw = await this.outbox.read.getRoots([BigInt(epoch)], opts);
99
+ return raw.map(hex => Fr.fromString(hex));
100
+ }
101
+
102
+ public consume(
103
+ message: ViemL2ToL1Msg,
104
+ epoch: EpochNumber,
105
+ numCheckpointsInEpoch: number,
106
+ leafIndex: bigint,
107
+ path: Hex[],
108
+ ) {
76
109
  const wallet = this.assertWallet();
77
- return wallet.write.consume([message, BigInt(epoch), leafIndex, path]);
110
+ return wallet.write.consume([message, BigInt(epoch), BigInt(numCheckpointsInEpoch), leafIndex, path]);
78
111
  }
79
112
 
80
113
  public async getMessageConsumedEvents(
81
114
  l1BlockHash: Hex,
82
- ): Promise<{ epoch: bigint; root: Hex; messageHash: Hex; leafId: bigint }[]> {
115
+ ): Promise<{ epoch: bigint; root: Hex; messageHash: Hex; leafId: bigint; numCheckpointsInEpoch: bigint }[]> {
83
116
  const events = await this.outbox.getEvents.MessageConsumed({}, { blockHash: l1BlockHash, strict: true });
84
117
  return events.map(event => ({
85
118
  epoch: event.args.epoch!,
86
119
  root: event.args.root!,
87
120
  messageHash: event.args.messageHash!,
88
121
  leafId: event.args.leafId!,
122
+ numCheckpointsInEpoch: event.args.numCheckpointsInEpoch!,
89
123
  }));
90
124
  }
91
125
 
@@ -6,6 +6,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import type { ViemSignature } from '@aztec/foundation/eth-signature';
7
7
  import { createLogger } from '@aztec/foundation/log';
8
8
  import { makeBackoff, retry } from '@aztec/foundation/retry';
9
+ import { getErrorCause } from '@aztec/foundation/types';
9
10
  import { EscapeHatchAbi } from '@aztec/l1-artifacts/EscapeHatchAbi';
10
11
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
11
12
  import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
@@ -13,11 +14,14 @@ import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
13
14
  import chunk from 'lodash.chunk';
14
15
  import {
15
16
  type Account,
17
+ ContractFunctionRevertedError,
16
18
  type GetContractReturnType,
17
19
  type Hex,
18
20
  type Log,
21
+ RpcRequestError,
19
22
  type StateOverride,
20
23
  type WatchContractEventReturnType,
24
+ decodeErrorResult,
21
25
  encodeAbiParameters,
22
26
  encodeFunctionData,
23
27
  getContract,
@@ -140,6 +144,20 @@ export enum TempCheckpointLogField {
140
144
  FeeHeader = 6,
141
145
  }
142
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
+
143
161
  /** Components of the minimum fee per mana, as returned by the L1 rollup contract. */
144
162
  export type ManaMinFeeComponents = {
145
163
  sequencerCost: bigint;
@@ -216,6 +234,32 @@ export type CheckpointProposedArgs = {
216
234
  /** Log type for CheckpointProposed events. */
217
235
  export type CheckpointProposedLog = L1EventLog<CheckpointProposedArgs>;
218
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
+
219
263
  export class RollupContract {
220
264
  private readonly rollup: GetContractReturnType<typeof RollupAbi, ViemClient>;
221
265
  private readonly logger = createLogger('ethereum:rollup');
@@ -248,7 +292,7 @@ export class RollupContract {
248
292
 
249
293
  static getFromConfig(config: L1ReaderConfig) {
250
294
  const client = getPublicClient(config);
251
- const address = config.l1Contracts.rollupAddress.toString();
295
+ const address = config.rollupAddress.toString();
252
296
  return new RollupContract(client, address);
253
297
  }
254
298
 
@@ -574,7 +618,7 @@ export class RollupContract {
574
618
  args: [timestamp],
575
619
  })
576
620
  .catch(e => {
577
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
621
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
578
622
  return { result: undefined };
579
623
  }
580
624
  throw e;
@@ -604,7 +648,7 @@ export class RollupContract {
604
648
  args: [],
605
649
  })
606
650
  .catch(e => {
607
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
651
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
608
652
  return { result: undefined };
609
653
  }
610
654
  throw e;
@@ -844,20 +888,32 @@ export class RollupContract {
844
888
  }
845
889
 
846
890
  /**
847
- * Returns a state override that sets the pending checkpoint number to the specified value. Useful for simulations.
848
- * Requires querying the current state of the contract to get the current proven checkpoint number, as they are both
849
- * 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`.
850
898
  */
851
- public async makePendingCheckpointNumberOverride(
852
- forcePendingCheckpointNumber: CheckpointNumber | undefined,
853
- ): Promise<StateOverride> {
854
- 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) {
855
904
  return [];
856
905
  }
857
906
  const slot = RollupContract.stfStorageSlot;
858
907
  const currentValue = await this.client.getStorageAt({ address: this.address, slot });
859
- const currentProvenCheckpointNumber = currentValue ? hexToBigInt(currentValue) & ((1n << 128n) - 1n) : 0n;
860
- 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;
861
917
  return [
862
918
  {
863
919
  address: this.address,
@@ -867,39 +923,56 @@ export class RollupContract {
867
923
  }
868
924
 
869
925
  /**
870
- * Returns a state override that sets tempCheckpointLogs[checkpointNumber].feeHeader to the compressed fee header.
871
- * Used when simulating a propose call where the parent checkpoint hasn't landed on L1 yet (pipelining).
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.
872
932
  */
873
- public async makeFeeHeaderOverride(checkpointNumber: CheckpointNumber, feeHeader: FeeHeader): Promise<StateOverride> {
874
- const { epochDuration, proofSubmissionEpochs } = await this.getRollupConstants();
875
- const roundaboutSize = BigInt(epochDuration * (proofSubmissionEpochs + 1) + 1);
876
- const circularIndex = BigInt(checkpointNumber) % roundaboutSize;
877
-
878
- // tempCheckpointLogs is at offset 2 in RollupStore
879
- const tempCheckpointLogsMappingBase = hexToBigInt(RollupContract.stfStorageSlot) + 2n;
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;
880
941
 
881
- // Solidity mapping slot: keccak256(abi.encode(key, baseSlot))
882
- const structBaseSlot = hexToBigInt(
883
- keccak256(
884
- encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [circularIndex, tempCheckpointLogsMappingBase]),
885
- ),
886
- );
942
+ const stateDiff: { slot: `0x${string}`; value: `0x${string}` }[] = [];
887
943
 
888
- // feeHeader is the 7th field (offset 6) in CompressedTempCheckpointLog
889
- const feeHeaderSlot = structBaseSlot + 6n;
890
- const compressed = RollupContract.compressFeeHeader(feeHeader);
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
+ }
891
971
 
892
- return [
893
- {
894
- address: this.address,
895
- stateDiff: [
896
- {
897
- slot: `0x${feeHeaderSlot.toString(16).padStart(64, '0')}`,
898
- value: `0x${compressed.toString(16).padStart(64, '0')}`,
899
- },
900
- ],
901
- },
902
- ];
972
+ if (stateDiff.length === 0) {
973
+ return [];
974
+ }
975
+ return [{ address: this.address, stateDiff }];
903
976
  }
904
977
 
905
978
  /**
@@ -1330,11 +1403,20 @@ export class RollupContract {
1330
1403
  checkpointNumber: CheckpointNumber,
1331
1404
  field: TempCheckpointLogField,
1332
1405
  ): Promise<bigint> {
1333
- const fieldOffset = BigInt(field);
1334
1406
  const [epochDuration, proofSubmissionEpochs] = await Promise.all([
1335
1407
  this.getEpochDuration(),
1336
1408
  this.getProofSubmissionEpochs(),
1337
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;
1338
1420
  const roundaboutSize = BigInt(epochDuration) * (BigInt(proofSubmissionEpochs) + 1n) + 1n;
1339
1421
  const tempCheckpointLogsBase = BigInt(RollupContract.stfStorageSlot) + 2n;
1340
1422
  const circularIndex = BigInt(checkpointNumber) % roundaboutSize;