@aztec/ethereum 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d

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 (144) hide show
  1. package/dest/client.d.ts +19 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +52 -3
  4. package/dest/config.d.ts +15 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +29 -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 +135 -35
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +226 -15
  18. package/dest/contracts/governance_proposer.d.ts +33 -4
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +52 -13
  21. package/dest/contracts/gse.d.ts +4 -2
  22. package/dest/contracts/gse.d.ts.map +1 -1
  23. package/dest/contracts/gse.js +2 -2
  24. package/dest/contracts/inbox.d.ts +3 -3
  25. package/dest/contracts/inbox.d.ts.map +1 -1
  26. package/dest/contracts/inbox.js +9 -3
  27. package/dest/contracts/index.d.ts +3 -3
  28. package/dest/contracts/index.d.ts.map +1 -1
  29. package/dest/contracts/index.js +2 -2
  30. package/dest/contracts/multicall.d.ts +79 -13
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +117 -95
  33. package/dest/contracts/outbox.d.ts +27 -4
  34. package/dest/contracts/outbox.d.ts.map +1 -1
  35. package/dest/contracts/outbox.js +34 -7
  36. package/dest/contracts/rollup.d.ts +5401 -34
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +271 -119
  39. package/dest/contracts/slashing_proposer.d.ts +171 -0
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/slashing_proposer.js +753 -0
  42. package/dest/contracts/watch_event.d.ts +36 -0
  43. package/dest/contracts/watch_event.d.ts.map +1 -0
  44. package/dest/contracts/watch_event.js +66 -0
  45. package/dest/deploy_aztec_l1_contracts.d.ts +4 -7
  46. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  47. package/dest/deploy_aztec_l1_contracts.js +15 -8
  48. package/dest/foundry_binary.d.ts +13 -0
  49. package/dest/foundry_binary.d.ts.map +1 -0
  50. package/dest/foundry_binary.js +52 -0
  51. package/dest/generated/l1-contracts-defaults.d.ts +2 -2
  52. package/dest/generated/l1-contracts-defaults.js +2 -2
  53. package/dest/l1_artifacts.d.ts +14941 -16273
  54. package/dest/l1_artifacts.d.ts.map +1 -1
  55. package/dest/l1_artifacts.js +9 -24
  56. package/dest/l1_contract_addresses.d.ts +61 -65
  57. package/dest/l1_contract_addresses.d.ts.map +1 -1
  58. package/dest/l1_contract_addresses.js +101 -82
  59. package/dest/l1_reader.d.ts +3 -5
  60. package/dest/l1_reader.d.ts.map +1 -1
  61. package/dest/l1_reader.js +3 -6
  62. package/dest/l1_tx_utils/config.js +3 -3
  63. package/dest/l1_tx_utils/l1_tx_utils.d.ts +7 -1
  64. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  65. package/dest/l1_tx_utils/l1_tx_utils.js +82 -29
  66. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  67. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  68. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +49 -19
  69. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  70. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  71. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  72. package/dest/publisher_manager.d.ts +18 -6
  73. package/dest/publisher_manager.d.ts.map +1 -1
  74. package/dest/publisher_manager.js +44 -7
  75. package/dest/queries.d.ts +13 -2
  76. package/dest/queries.d.ts.map +1 -1
  77. package/dest/queries.js +62 -3
  78. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  79. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  80. package/dest/test/blob_kzg_warmup.js +64 -0
  81. package/dest/test/chain_monitor.d.ts +34 -2
  82. package/dest/test/chain_monitor.d.ts.map +1 -1
  83. package/dest/test/chain_monitor.js +76 -6
  84. package/dest/test/eth_cheat_codes.d.ts +26 -3
  85. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  86. package/dest/test/eth_cheat_codes.js +61 -40
  87. package/dest/test/index.d.ts +2 -1
  88. package/dest/test/index.d.ts.map +1 -1
  89. package/dest/test/index.js +1 -0
  90. package/dest/test/recording_rpc_server.d.ts +18 -0
  91. package/dest/test/recording_rpc_server.d.ts.map +1 -0
  92. package/dest/test/recording_rpc_server.js +68 -0
  93. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  94. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  95. package/dest/test/rollup_cheat_codes.js +86 -7
  96. package/dest/test/start_anvil.d.ts +1 -1
  97. package/dest/test/start_anvil.d.ts.map +1 -1
  98. package/dest/test/start_anvil.js +41 -5
  99. package/dest/utils.d.ts +9 -2
  100. package/dest/utils.d.ts.map +1 -1
  101. package/dest/utils.js +45 -14
  102. package/package.json +6 -6
  103. package/src/client.ts +63 -1
  104. package/src/config.ts +44 -13
  105. package/src/contracts/chain_state_override.ts +200 -0
  106. package/src/contracts/empire_base.ts +0 -2
  107. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  108. package/src/contracts/governance.ts +310 -10
  109. package/src/contracts/governance_proposer.ts +56 -13
  110. package/src/contracts/gse.ts +7 -2
  111. package/src/contracts/inbox.ts +12 -4
  112. package/src/contracts/index.ts +2 -2
  113. package/src/contracts/multicall.ts +179 -105
  114. package/src/contracts/outbox.ts +42 -8
  115. package/src/contracts/rollup.ts +302 -119
  116. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +119 -45
  117. package/src/contracts/watch_event.ts +110 -0
  118. package/src/deploy_aztec_l1_contracts.ts +19 -10
  119. package/src/foundry_binary.ts +57 -0
  120. package/src/generated/l1-contracts-defaults.ts +2 -2
  121. package/src/l1_artifacts.ts +12 -35
  122. package/src/l1_contract_addresses.ts +120 -94
  123. package/src/l1_reader.ts +4 -9
  124. package/src/l1_tx_utils/config.ts +3 -3
  125. package/src/l1_tx_utils/l1_tx_utils.ts +83 -36
  126. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +49 -13
  127. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  128. package/src/publisher_manager.ts +54 -18
  129. package/src/queries.ts +70 -4
  130. package/src/test/blob_kzg_warmup.ts +65 -0
  131. package/src/test/chain_monitor.ts +93 -5
  132. package/src/test/eth_cheat_codes.ts +57 -43
  133. package/src/test/index.ts +1 -0
  134. package/src/test/recording_rpc_server.ts +69 -0
  135. package/src/test/rollup_cheat_codes.ts +115 -6
  136. package/src/test/start_anvil.ts +38 -6
  137. package/src/utils.ts +41 -17
  138. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  139. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  140. package/dest/contracts/empire_slashing_proposer.js +0 -216
  141. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  142. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  143. package/dest/contracts/tally_slashing_proposer.js +0 -320
  144. package/src/contracts/empire_slashing_proposer.ts +0 -265
@@ -15,11 +15,19 @@ import {
15
15
  import type { L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
16
16
  import type { ViemClient } from '../types.js';
17
17
  import { type IEmpireBase, encodeSignal, encodeSignalWithSignature, signSignalWithSig } from './empire_base.js';
18
- import { extractProposalIdFromLogs } from './governance.js';
18
+ import { type PayloadProposalStatus, ReadOnlyGovernanceContract, extractProposalIdFromLogs } from './governance.js';
19
19
 
20
20
  export class GovernanceProposerContract implements IEmpireBase {
21
21
  private readonly proposer: GetContractReturnType<typeof GovernanceProposerAbi, ViemClient>;
22
22
 
23
+ /**
24
+ * Cache of bytecode-existence checks keyed by payload address. The check is stable for a
25
+ * contract's lifetime -- a contract either has code or it does not, and code cannot be removed
26
+ * after deployment (selfdestruct aside, which is not relevant here). Safe to memoize
27
+ * indefinitely for the lifetime of this instance.
28
+ */
29
+ private readonly emptyPayloadCache: Map<Hex, boolean> = new Map();
30
+
23
31
  constructor(
24
32
  public readonly client: ViemClient,
25
33
  address: Hex | EthAddress,
@@ -30,16 +38,16 @@ export class GovernanceProposerContract implements IEmpireBase {
30
38
  this.proposer = getContract({ address, abi: GovernanceProposerAbi, client });
31
39
  }
32
40
 
33
- public get address() {
41
+ public get address(): EthAddress {
34
42
  return EthAddress.fromString(this.proposer.address);
35
43
  }
36
44
 
37
- public async getRollupAddress() {
45
+ public async getRollupAddress(): Promise<EthAddress> {
38
46
  return EthAddress.fromString(await this.proposer.read.getInstance());
39
47
  }
40
48
 
41
49
  @memoize
42
- public async getRegistryAddress() {
50
+ public async getRegistryAddress(): Promise<EthAddress> {
43
51
  return EthAddress.fromString(await this.proposer.read.REGISTRY());
44
52
  }
45
53
 
@@ -51,10 +59,6 @@ export class GovernanceProposerContract implements IEmpireBase {
51
59
  return this.proposer.read.ROUND_SIZE();
52
60
  }
53
61
 
54
- public getInstance() {
55
- return this.proposer.read.getInstance();
56
- }
57
-
58
62
  public computeRound(slot: SlotNumber): Promise<bigint> {
59
63
  return this.proposer.read.computeRound([BigInt(slot)]);
60
64
  }
@@ -99,7 +103,7 @@ export class GovernanceProposerContract implements IEmpireBase {
99
103
  signer,
100
104
  payload,
101
105
  slot,
102
- await this.getInstance(),
106
+ (await this.getRollupAddress()).toString(),
103
107
  this.address.toString(),
104
108
  chainId,
105
109
  );
@@ -110,10 +114,49 @@ export class GovernanceProposerContract implements IEmpireBase {
110
114
  };
111
115
  }
112
116
 
113
- /** Checks if a payload was ever submitted to governance via submitRoundWinner. */
114
- public async hasPayloadBeenProposed(payload: Hex, fromBlock: bigint): Promise<boolean> {
115
- const events = await this.proposer.getEvents.PayloadSubmitted({ payload }, { fromBlock, strict: true });
116
- return events.length > 0;
117
+ /**
118
+ * Resolves the Governance contract this proposer submits winners to. Lazily reads
119
+ * `GovernanceProposer.getGovernance()` (which itself looks the address up via the registry) and
120
+ * memoizes the resulting wrapper.
121
+ */
122
+ @memoize
123
+ public async getGovernance(): Promise<ReadOnlyGovernanceContract> {
124
+ const address = await this.proposer.read.getGovernance();
125
+ return new ReadOnlyGovernanceContract(address, this.client);
126
+ }
127
+
128
+ /**
129
+ * Classifies the given original payload against the Governance proposal history (`'live'` /
130
+ * `'executed'` / `'none'`). Delegates to `ReadOnlyGovernanceContract.getPayloadProposalStatus`,
131
+ * which implements the actual sweep against the Governance contract -- this method exists only as
132
+ * a convenience wrapper so callers that already hold a GovernanceProposer reference don't have to
133
+ * resolve the Governance address themselves.
134
+ */
135
+ public async getPayloadProposalStatus(payload: Hex): Promise<PayloadProposalStatus> {
136
+ const governance = await this.getGovernance();
137
+ return governance.getPayloadProposalStatus(payload);
138
+ }
139
+
140
+ /**
141
+ * Returns true if the given payload address has no deployed bytecode. Used as a cheap
142
+ * pre-flight check before casting a governance signal — voting for a zero-code address
143
+ * is unrecoverable.
144
+ *
145
+ * We only cache the `false` result (address has bytecode). The `true` result is NOT
146
+ * cached because a CREATE2-redeployed address could go from empty to populated, and
147
+ * caching `true` would make us keep skipping a payload that later becomes valid.
148
+ */
149
+ public async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
150
+ const key = payload.toString() as Hex;
151
+ if (this.emptyPayloadCache.get(key) === false) {
152
+ return false;
153
+ }
154
+ const code = await this.client.getCode({ address: key });
155
+ const isEmpty = !code || code === '0x';
156
+ if (!isEmpty) {
157
+ this.emptyPayloadCache.set(key, false);
158
+ }
159
+ return isEmpty;
117
160
  }
118
161
 
119
162
  public async submitRoundWinner(
@@ -47,11 +47,16 @@ export class GSEContract {
47
47
  return EthAddress.fromString(await this.gse.read.getGovernance());
48
48
  }
49
49
 
50
- getAttestersFromIndicesAtTime(instance: Hex | EthAddress, ts: bigint, indices: bigint[]) {
50
+ getAttestersFromIndicesAtTime(
51
+ instance: Hex | EthAddress,
52
+ ts: bigint,
53
+ indices: bigint[],
54
+ options?: { blockNumber?: bigint },
55
+ ) {
51
56
  if (instance instanceof EthAddress) {
52
57
  instance = instance.toString();
53
58
  }
54
- return this.gse.read.getAttestersFromIndicesAtTime([instance, ts, indices]);
59
+ return this.gse.read.getAttestersFromIndicesAtTime([instance, ts, indices], options);
55
60
  }
56
61
 
57
62
  public async getRegistrationDigest(publicKey: ProjPointType<bigint>): Promise<ProjPointType<bigint>> {
@@ -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,9 +83,16 @@ export class InboxContract {
82
83
  .map(log => this.mapMessageSentLog(log));
83
84
  }
84
85
 
85
- /** Fetches MessageSent events for a specific message hash at a specific block. */
86
- async getMessageSentEventByHash(msgHash: Hex, l1BlockHash: Hex): Promise<MessageSentLog> {
87
- const [log] = await this.inbox.getEvents.MessageSent({ hash: msgHash }, { blockHash: l1BlockHash });
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
+ );
88
96
  return log && this.mapMessageSentLog(log);
89
97
  }
90
98
 
@@ -1,3 +1,4 @@
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';
@@ -12,6 +13,5 @@ export * from './multicall.js';
12
13
  export * from './outbox.js';
13
14
  export * from './registry.js';
14
15
  export * from './rollup.js';
15
- export * from './empire_slashing_proposer.js';
16
- export * from './tally_slashing_proposer.js';
16
+ export * from './slashing_proposer.js';
17
17
  export * from './slasher_contract.js';
@@ -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