@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.
- package/dest/client.d.ts +11 -3
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +35 -3
- package/dest/contracts/chain_state_override.d.ts +47 -7
- package/dest/contracts/chain_state_override.d.ts.map +1 -1
- package/dest/contracts/chain_state_override.js +57 -20
- package/dest/contracts/empire_base.d.ts +1 -3
- package/dest/contracts/empire_base.d.ts.map +1 -1
- package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
- package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
- package/dest/contracts/fee_asset_price_oracle.js +11 -6
- package/dest/contracts/governance.d.ts +108 -33
- package/dest/contracts/governance.d.ts.map +1 -1
- package/dest/contracts/governance.js +193 -12
- package/dest/contracts/governance_proposer.d.ts +33 -3
- package/dest/contracts/governance_proposer.d.ts.map +1 -1
- package/dest/contracts/governance_proposer.js +51 -9
- package/dest/contracts/inbox.js +1 -1
- package/dest/contracts/multicall.d.ts +79 -13
- package/dest/contracts/multicall.d.ts.map +1 -1
- package/dest/contracts/multicall.js +117 -95
- package/dest/contracts/outbox.d.ts +27 -4
- package/dest/contracts/outbox.d.ts.map +1 -1
- package/dest/contracts/outbox.js +34 -7
- package/dest/contracts/rollup.d.ts +327 -57
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +102 -40
- package/dest/l1_artifacts.d.ts +5108 -155
- package/dest/l1_artifacts.d.ts.map +1 -1
- package/dest/l1_contract_addresses.d.ts +61 -61
- package/dest/l1_contract_addresses.d.ts.map +1 -1
- package/dest/l1_contract_addresses.js +101 -76
- package/dest/l1_reader.d.ts +3 -5
- package/dest/l1_reader.d.ts.map +1 -1
- package/dest/l1_reader.js +3 -6
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +7 -1
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.js +21 -10
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
- package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
- package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
- package/dest/l1_tx_utils/tx_delayer.js +7 -5
- package/dest/test/eth_cheat_codes.d.ts +7 -1
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +11 -0
- package/dest/test/rollup_cheat_codes.d.ts +27 -3
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
- package/dest/test/rollup_cheat_codes.js +50 -6
- package/dest/utils.d.ts +9 -2
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +34 -6
- package/package.json +6 -6
- package/src/client.ts +39 -1
- package/src/contracts/chain_state_override.ts +77 -24
- package/src/contracts/empire_base.ts +0 -2
- package/src/contracts/fee_asset_price_oracle.ts +10 -5
- package/src/contracts/governance.ts +266 -9
- package/src/contracts/governance_proposer.ts +52 -5
- package/src/contracts/inbox.ts +1 -1
- package/src/contracts/multicall.ts +179 -105
- package/src/contracts/outbox.ts +42 -8
- package/src/contracts/rollup.ts +124 -42
- package/src/l1_contract_addresses.ts +120 -87
- package/src/l1_reader.ts +4 -9
- package/src/l1_tx_utils/l1_tx_utils.ts +27 -14
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
- package/src/l1_tx_utils/tx_delayer.ts +16 -7
- package/src/test/eth_cheat_codes.ts +10 -0
- package/src/test/rollup_cheat_codes.ts +53 -5
- package/src/utils.ts +30 -6
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
logger: Logger,
|
|
55
|
-
opts: { revertOnFailure?: boolean } = {},
|
|
193
|
+
opts: { gasLimitRequired?: TOptGasLimitRequired } = {},
|
|
56
194
|
) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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. */
|
package/src/contracts/outbox.ts
CHANGED
|
@@ -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.
|
|
42
|
+
const address = config.outboxAddress.toString();
|
|
42
43
|
return new OutboxContract(client, address);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
package/src/contracts/rollup.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
848
|
-
*
|
|
849
|
-
*
|
|
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
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
|
860
|
-
const
|
|
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
|
|
871
|
-
*
|
|
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
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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;
|