@aztec/ethereum 0.0.1-commit.e558bd1c → 0.0.1-commit.e57c76e

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 (159) hide show
  1. package/dest/client.d.ts +25 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +60 -8
  4. package/dest/config.d.ts +8 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +15 -11
  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/fee_asset_price_oracle.d.ts +105 -0
  11. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
  12. package/dest/contracts/fee_asset_price_oracle.js +656 -0
  13. package/dest/contracts/governance.d.ts +108 -33
  14. package/dest/contracts/governance.d.ts.map +1 -1
  15. package/dest/contracts/governance.js +196 -15
  16. package/dest/contracts/governance_proposer.d.ts +33 -1
  17. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  18. package/dest/contracts/governance_proposer.js +53 -2
  19. package/dest/contracts/inbox.d.ts +3 -3
  20. package/dest/contracts/inbox.d.ts.map +1 -1
  21. package/dest/contracts/inbox.js +12 -7
  22. package/dest/contracts/index.d.ts +4 -3
  23. package/dest/contracts/index.d.ts.map +1 -1
  24. package/dest/contracts/index.js +3 -2
  25. package/dest/contracts/multicall.d.ts +127 -12
  26. package/dest/contracts/multicall.d.ts.map +1 -1
  27. package/dest/contracts/multicall.js +201 -94
  28. package/dest/contracts/outbox.d.ts +27 -4
  29. package/dest/contracts/outbox.d.ts.map +1 -1
  30. package/dest/contracts/outbox.js +34 -7
  31. package/dest/contracts/registry.d.ts +3 -1
  32. package/dest/contracts/registry.d.ts.map +1 -1
  33. package/dest/contracts/registry.js +30 -1
  34. package/dest/contracts/rollup.d.ts +5259 -29
  35. package/dest/contracts/rollup.d.ts.map +1 -1
  36. package/dest/contracts/rollup.js +388 -71
  37. package/dest/contracts/{tally_slashing_proposer.d.ts → slashing_proposer.d.ts} +3 -4
  38. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  39. package/dest/contracts/{tally_slashing_proposer.js → slashing_proposer.js} +13 -15
  40. package/dest/deploy_aztec_l1_contracts.d.ts +4 -8
  41. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  42. package/dest/deploy_aztec_l1_contracts.js +35 -23
  43. package/dest/deploy_l1_contract.js +3 -3
  44. package/dest/generated/l1-contracts-defaults.d.ts +2 -2
  45. package/dest/generated/l1-contracts-defaults.js +2 -2
  46. package/dest/l1_artifacts.d.ts +14259 -15724
  47. package/dest/l1_artifacts.d.ts.map +1 -1
  48. package/dest/l1_artifacts.js +9 -24
  49. package/dest/l1_contract_addresses.d.ts +61 -65
  50. package/dest/l1_contract_addresses.d.ts.map +1 -1
  51. package/dest/l1_contract_addresses.js +101 -82
  52. package/dest/l1_reader.d.ts +5 -5
  53. package/dest/l1_reader.d.ts.map +1 -1
  54. package/dest/l1_reader.js +9 -7
  55. package/dest/l1_tx_utils/config.d.ts +7 -1
  56. package/dest/l1_tx_utils/config.d.ts.map +1 -1
  57. package/dest/l1_tx_utils/config.js +14 -1
  58. package/dest/l1_tx_utils/factory.d.ts +18 -10
  59. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  60. package/dest/l1_tx_utils/factory.js +17 -7
  61. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +1 -1
  62. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +1 -1
  63. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts +15 -15
  64. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts.map +1 -1
  65. package/dest/l1_tx_utils/forwarder_l1_tx_utils.js +9 -15
  66. package/dest/l1_tx_utils/index-blobs.d.ts +3 -3
  67. package/dest/l1_tx_utils/index-blobs.d.ts.map +1 -1
  68. package/dest/l1_tx_utils/index-blobs.js +2 -2
  69. package/dest/l1_tx_utils/index.d.ts +2 -1
  70. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  71. package/dest/l1_tx_utils/index.js +1 -0
  72. package/dest/l1_tx_utils/l1_tx_utils.d.ts +26 -7
  73. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  74. package/dest/l1_tx_utils/l1_tx_utils.js +95 -60
  75. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  76. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  77. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +10 -5
  78. package/dest/l1_tx_utils/tx_delayer.d.ts +58 -0
  79. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -0
  80. package/dest/{test → l1_tx_utils}/tx_delayer.js +67 -37
  81. package/dest/publisher_manager.d.ts +21 -7
  82. package/dest/publisher_manager.d.ts.map +1 -1
  83. package/dest/publisher_manager.js +81 -7
  84. package/dest/queries.d.ts +13 -2
  85. package/dest/queries.d.ts.map +1 -1
  86. package/dest/queries.js +62 -3
  87. package/dest/test/chain_monitor.d.ts +23 -3
  88. package/dest/test/chain_monitor.d.ts.map +1 -1
  89. package/dest/test/chain_monitor.js +38 -2
  90. package/dest/test/eth_cheat_codes.d.ts +12 -4
  91. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  92. package/dest/test/eth_cheat_codes.js +17 -4
  93. package/dest/test/index.d.ts +1 -3
  94. package/dest/test/index.d.ts.map +1 -1
  95. package/dest/test/index.js +0 -2
  96. package/dest/test/rollup_cheat_codes.d.ts +31 -3
  97. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  98. package/dest/test/rollup_cheat_codes.js +59 -7
  99. package/dest/test/start_anvil.d.ts +23 -3
  100. package/dest/test/start_anvil.d.ts.map +1 -1
  101. package/dest/test/start_anvil.js +143 -29
  102. package/dest/test/upgrade_utils.js +2 -2
  103. package/dest/utils.d.ts +9 -2
  104. package/dest/utils.d.ts.map +1 -1
  105. package/dest/utils.js +48 -16
  106. package/package.json +6 -8
  107. package/src/client.ts +66 -2
  108. package/src/config.ts +24 -15
  109. package/src/contracts/chain_state_override.ts +200 -0
  110. package/src/contracts/fee_asset_price_oracle.ts +285 -0
  111. package/src/contracts/governance.ts +269 -12
  112. package/src/contracts/governance_proposer.ts +54 -1
  113. package/src/contracts/inbox.ts +13 -5
  114. package/src/contracts/index.ts +3 -2
  115. package/src/contracts/multicall.ts +243 -105
  116. package/src/contracts/outbox.ts +42 -8
  117. package/src/contracts/registry.ts +31 -1
  118. package/src/contracts/rollup.ts +439 -73
  119. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +14 -16
  120. package/src/deploy_aztec_l1_contracts.ts +57 -34
  121. package/src/deploy_l1_contract.ts +3 -3
  122. package/src/generated/l1-contracts-defaults.ts +2 -2
  123. package/src/l1_artifacts.ts +12 -35
  124. package/src/l1_contract_addresses.ts +120 -94
  125. package/src/l1_reader.ts +17 -10
  126. package/src/l1_tx_utils/config.ts +20 -0
  127. package/src/l1_tx_utils/factory.ts +31 -31
  128. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +1 -1
  129. package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +1 -1
  130. package/src/l1_tx_utils/forwarder_l1_tx_utils.ts +43 -54
  131. package/src/l1_tx_utils/index-blobs.ts +2 -2
  132. package/src/l1_tx_utils/index.ts +1 -0
  133. package/src/l1_tx_utils/l1_tx_utils.ts +93 -42
  134. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +10 -5
  135. package/src/{test → l1_tx_utils}/tx_delayer.ts +93 -56
  136. package/src/publisher_manager.ts +105 -10
  137. package/src/queries.ts +70 -4
  138. package/src/test/chain_monitor.ts +65 -3
  139. package/src/test/eth_cheat_codes.ts +16 -4
  140. package/src/test/index.ts +0 -2
  141. package/src/test/rollup_cheat_codes.ts +64 -6
  142. package/src/test/start_anvil.ts +177 -29
  143. package/src/test/upgrade_utils.ts +2 -2
  144. package/src/utils.ts +45 -18
  145. package/dest/contracts/empire_slashing_proposer.d.ts +0 -67
  146. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  147. package/dest/contracts/empire_slashing_proposer.js +0 -207
  148. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  149. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +0 -26
  150. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +0 -1
  151. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +0 -26
  152. package/dest/test/delayed_tx_utils.d.ts +0 -13
  153. package/dest/test/delayed_tx_utils.d.ts.map +0 -1
  154. package/dest/test/delayed_tx_utils.js +0 -28
  155. package/dest/test/tx_delayer.d.ts +0 -36
  156. package/dest/test/tx_delayer.d.ts.map +0 -1
  157. package/src/contracts/empire_slashing_proposer.ts +0 -259
  158. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +0 -77
  159. package/src/test/delayed_tx_utils.ts +0 -52
@@ -1,3 +1,4 @@
1
+ import { maxBigint } from '@aztec/foundation/bigint';
1
2
  import { CheckpointNumber } from '@aztec/foundation/branded-types';
2
3
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
3
4
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -37,7 +38,7 @@ export class InboxContract {
37
38
 
38
39
  static getFromConfig(config: L1ReaderConfig) {
39
40
  const client = getPublicClient(config);
40
- const address = config.l1Contracts.inboxAddress.toString();
41
+ const address = config.inboxAddress.toString();
41
42
  return new InboxContract(client, address);
42
43
  }
43
44
 
@@ -82,10 +83,17 @@ export class InboxContract {
82
83
  .map(log => this.mapMessageSentLog(log));
83
84
  }
84
85
 
85
- /** Fetches MessageSent events for a specific message hash within the given block range. */
86
- async getMessageSentEventByHash(hash: Hex, fromBlock: bigint, toBlock: bigint): Promise<MessageSentLog[]> {
87
- const logs = await this.inbox.getEvents.MessageSent({ hash }, { fromBlock, toBlock });
88
- return logs.map(log => this.mapMessageSentLog(log));
86
+ /** Fetches MessageSent events for a specific message hash around a specific block. */
87
+ async getMessageSentEventByHash(msgHash: Hex, aroundL1BlockNumber: bigint): Promise<MessageSentLog> {
88
+ // We don't use blockHash here because we don't want the query to throw if the L1 block number no longer exists on chain
89
+ // due to an L1 reorg. The use case for this method is usually checking if a message still exists on the Inbox after
90
+ // a reorg, so it's possible the message was moved one block up or down, and that the original L1 block where we
91
+ // saw it no longer exists, rendering the block-by-hash approach invalid.
92
+ const [log] = await this.inbox.getEvents.MessageSent(
93
+ { hash: msgHash },
94
+ { fromBlock: maxBigint(aroundL1BlockNumber - 5n, 1n), toBlock: aroundL1BlockNumber + 5n },
95
+ );
96
+ return log && this.mapMessageSentLog(log);
89
97
  }
90
98
 
91
99
  private mapMessageSentLog(log: {
@@ -1,6 +1,8 @@
1
+ export * from './chain_state_override.js';
1
2
  export * from './empire_base.js';
2
3
  export * from './errors.js';
3
4
  export * from './fee_asset_handler.js';
5
+ export * from './fee_asset_price_oracle.js';
4
6
  export * from './fee_juice.js';
5
7
  export * from './governance.js';
6
8
  export * from './governance_proposer.js';
@@ -11,6 +13,5 @@ export * from './multicall.js';
11
13
  export * from './outbox.js';
12
14
  export * from './registry.js';
13
15
  export * from './rollup.js';
14
- export * from './empire_slashing_proposer.js';
15
- export * from './tally_slashing_proposer.js';
16
+ export * from './slashing_proposer.js';
16
17
  export * from './slasher_contract.js';
@@ -1,126 +1,264 @@
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 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
+
35
+ /** ABI fragment for aggregate3Value — not included in viem's multicall3Abi. */
36
+ export const aggregate3ValueAbi = [
37
+ {
38
+ inputs: [
39
+ {
40
+ components: [
41
+ { internalType: 'address', name: 'target', type: 'address' },
42
+ { internalType: 'bool', name: 'allowFailure', type: 'bool' },
43
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
44
+ { internalType: 'bytes', name: 'callData', type: 'bytes' },
45
+ ],
46
+ internalType: 'struct Multicall3.Call3Value[]',
47
+ name: 'calls',
48
+ type: 'tuple[]',
49
+ },
50
+ ],
51
+ name: 'aggregate3Value',
52
+ outputs: [
53
+ {
54
+ components: [
55
+ { internalType: 'bool', name: 'success', type: 'bool' },
56
+ { internalType: 'bytes', name: 'returnData', type: 'bytes' },
57
+ ],
58
+ internalType: 'struct Multicall3.Result[]',
59
+ name: 'returnData',
60
+ type: 'tuple[]',
61
+ },
62
+ ],
63
+ stateMutability: 'payable',
64
+ type: 'function',
65
+ },
66
+ ] as const;
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
+
14
109
  export class Multicall3 {
15
- 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>(
16
189
  requests: L1TxRequest[],
17
190
  l1TxUtils: L1TxUtils,
18
- gasConfig: L1TxConfig | undefined,
191
+ gasConfig: TOptGasLimitRequired extends true ? RequiredBy<L1TxConfig, 'gasLimit'> : L1TxConfig | undefined,
19
192
  blobConfig: L1BlobInputs | undefined,
20
- rollupAddress: Hex,
21
- logger: Logger,
22
- opts: { revertOnFailure?: boolean } = {},
193
+ opts: { gasLimitRequired?: TOptGasLimitRequired } = {},
23
194
  ) {
24
- requests = requests.filter(request => request.to !== null);
25
- const args = requests.map(r => ({
26
- target: r.to!,
27
- callData: r.data!,
28
- allowFailure: !opts.revertOnFailure,
29
- }));
30
- 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({
31
207
  abi: multicall3Abi,
32
208
  functionName: 'aggregate3',
33
209
  args: [args],
34
- };
35
-
36
- const encodedForwarderData = encodeFunctionData(forwarderFunctionData);
37
- try {
38
- const { receipt, state } = await l1TxUtils.sendAndMonitorTransaction(
39
- {
40
- to: MULTI_CALL_3_ADDRESS,
41
- data: encodedForwarderData,
42
- abi: multicall3Abi,
43
- },
44
- gasConfig,
45
- blobConfig,
46
- );
47
-
48
- if (receipt.status === 'success') {
49
- const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
50
- return { receipt, stats };
51
- } else {
52
- logger.error('Forwarder transaction failed', undefined, { receipt });
53
-
54
- const args = {
55
- ...forwarderFunctionData,
56
- address: MULTI_CALL_3_ADDRESS,
57
- };
58
-
59
- let errorMsg: string | undefined;
60
-
61
- if (blobConfig) {
62
- const maxFeePerBlobGas = blobConfig.maxFeePerBlobGas ?? state.gasPrice.maxFeePerBlobGas;
63
- if (maxFeePerBlobGas === undefined) {
64
- errorMsg = 'maxFeePerBlobGas is required to get the error message';
65
- } else {
66
- logger.debug('Trying to get error from reverted tx with blob config');
67
- errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(
68
- encodedForwarderData,
69
- args,
70
- {
71
- blobs: blobConfig.blobs,
72
- kzg: blobConfig.kzg,
73
- maxFeePerBlobGas,
74
- },
75
- [
76
- {
77
- address: rollupAddress,
78
- stateDiff: [
79
- {
80
- slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
81
- value: toPaddedHex(0n, true),
82
- },
83
- ],
84
- },
85
- ],
86
- );
87
- }
88
- } else {
89
- logger.debug('Trying to get error from reverted tx without blob config');
90
- errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(encodedForwarderData, args, undefined, []);
91
- }
92
-
93
- return { receipt, errorMsg };
94
- }
95
- } catch (err) {
96
- if (err instanceof TimeoutError) {
97
- throw err;
98
- }
210
+ });
99
211
 
100
- for (const request of requests) {
101
- logger.debug('Simulating request', { request });
102
- const result = await l1TxUtils
103
- .simulate(request, undefined, [
104
- {
105
- address: rollupAddress,
106
- stateDiff: [
107
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
108
- ],
109
- },
110
- ])
111
- .catch(err => formatViemError(err, request.abi));
112
- if (result instanceof FormattedViemError) {
113
- logger.error('Found error in simulation', result, {
114
- to: request.to ?? 'null',
115
- data: request.data,
116
- });
117
-
118
- return result;
119
- }
120
- }
121
- logger.warn('Failed to get error from reverted tx', { err });
122
- 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);
123
227
  }
228
+
229
+ const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
230
+ return { receipt, stats, multicallData: encodedForwarderData };
231
+ }
232
+
233
+ /** Batch multiple value transfers into a single aggregate3Value call on Multicall3. */
234
+ static async forwardValue(calls: { to: Address; value: bigint }[], l1TxUtils: L1TxUtils, logger: Logger) {
235
+ const args = calls.map(c => ({
236
+ target: c.to,
237
+ allowFailure: false,
238
+ value: c.value,
239
+ callData: '0x' as Hex,
240
+ }));
241
+
242
+ const data = encodeFunctionData({
243
+ abi: aggregate3ValueAbi,
244
+ functionName: 'aggregate3Value',
245
+ args: [args],
246
+ });
247
+
248
+ const totalValue = calls.reduce((sum, c) => sum + c.value, 0n);
249
+
250
+ logger.info(`Sending aggregate3Value with ${calls.length} calls`, { totalValue });
251
+ const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
252
+ to: MULTI_CALL_3_ADDRESS,
253
+ data,
254
+ value: totalValue,
255
+ });
256
+
257
+ if (receipt.status !== 'success') {
258
+ throw new Error(`aggregate3Value transaction reverted: ${receipt.transactionHash}`);
259
+ }
260
+
261
+ return { receipt };
124
262
  }
125
263
  }
126
264
 
@@ -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
 
@@ -3,7 +3,7 @@ import { createLogger } from '@aztec/foundation/log';
3
3
  import { RegistryAbi } from '@aztec/l1-artifacts/RegistryAbi';
4
4
  import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi';
5
5
 
6
- import { type GetContractReturnType, type Hex, getContract } from 'viem';
6
+ import { type GetContractReturnType, type Hex, getAbiItem, getContract } from 'viem';
7
7
 
8
8
  import type { L1ContractAddresses } from '../l1_contract_addresses.js';
9
9
  import type { ViemClient } from '../types.js';
@@ -128,4 +128,34 @@ export class RegistryContract {
128
128
  public async getRewardDistributor(): Promise<EthAddress> {
129
129
  return EthAddress.fromString(await this.registry.read.getRewardDistributor());
130
130
  }
131
+
132
+ /** Returns the L1 timestamp at which the given rollup was registered via addRollup(). */
133
+ public async getCanonicalRollupRegistrationTimestamp(
134
+ rollupAddress: EthAddress,
135
+ fromBlock?: bigint,
136
+ ): Promise<bigint | undefined> {
137
+ const event = getAbiItem({ abi: RegistryAbi, name: 'CanonicalRollupUpdated' });
138
+ const start = fromBlock ?? 0n;
139
+ const latestBlock = await this.client.getBlockNumber();
140
+ const chunkSize = 1_000n;
141
+
142
+ for (let from = start; from <= latestBlock; from += chunkSize) {
143
+ const to = from + chunkSize - 1n > latestBlock ? latestBlock : from + chunkSize - 1n;
144
+ const logs = await this.client.getLogs({
145
+ address: this.address.toString(),
146
+ fromBlock: from,
147
+ toBlock: to,
148
+ strict: true,
149
+ event,
150
+ args: { instance: rollupAddress.toString() },
151
+ });
152
+
153
+ if (logs.length > 0) {
154
+ const block = await this.client.getBlock({ blockNumber: logs[0].blockNumber });
155
+ return block.timestamp;
156
+ }
157
+ }
158
+
159
+ return undefined;
160
+ }
131
161
  }