@aztec/sequencer-client 0.56.0 → 0.58.0
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/block_builder/index.d.ts +2 -2
- package/dest/block_builder/index.d.ts.map +1 -1
- package/dest/block_builder/light.d.ts +8 -12
- package/dest/block_builder/light.d.ts.map +1 -1
- package/dest/block_builder/light.js +11 -15
- package/dest/block_builder/orchestrator.d.ts +8 -11
- package/dest/block_builder/orchestrator.d.ts.map +1 -1
- package/dest/block_builder/orchestrator.js +3 -10
- package/dest/client/sequencer-client.d.ts +2 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +3 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -9
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +2 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +7 -2
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/l1-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/l1-publisher-metrics.js +2 -1
- package/dest/publisher/l1-publisher.d.ts +52 -36
- package/dest/publisher/l1-publisher.d.ts.map +1 -1
- package/dest/publisher/l1-publisher.js +305 -117
- package/dest/publisher/utils.d.ts +1 -1
- package/dest/publisher/utils.js +1 -1
- package/dest/sequencer/sequencer.d.ts +6 -6
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +142 -88
- package/dest/tx_validator/gas_validator.d.ts.map +1 -1
- package/dest/tx_validator/gas_validator.js +7 -5
- package/dest/tx_validator/phases_validator.d.ts +1 -1
- package/dest/tx_validator/phases_validator.d.ts.map +1 -1
- package/dest/tx_validator/phases_validator.js +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +9 -7
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +20 -10
- package/package.json +22 -19
- package/src/block_builder/index.ts +2 -2
- package/src/block_builder/light.ts +16 -24
- package/src/block_builder/orchestrator.ts +10 -18
- package/src/client/sequencer-client.ts +3 -9
- package/src/config.ts +10 -8
- package/src/global_variable_builder/global_builder.ts +1 -0
- package/src/publisher/config.ts +6 -1
- package/src/publisher/index.ts +1 -1
- package/src/publisher/l1-publisher-metrics.ts +1 -0
- package/src/publisher/l1-publisher.ts +435 -144
- package/src/publisher/utils.ts +1 -1
- package/src/sequencer/sequencer.ts +173 -108
- package/src/tx_validator/gas_validator.ts +6 -7
- package/src/tx_validator/phases_validator.ts +1 -1
- package/src/tx_validator/tx_validator_factory.ts +39 -15
|
@@ -1,44 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ConsensusPayload,
|
|
3
|
+
type EpochProofClaim,
|
|
4
|
+
type EpochProofQuote,
|
|
5
|
+
type L2Block,
|
|
6
|
+
type TxHash,
|
|
7
|
+
getHashedSignaturePayload,
|
|
8
|
+
} from '@aztec/circuit-types';
|
|
2
9
|
import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats';
|
|
3
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AGGREGATION_OBJECT_LENGTH,
|
|
12
|
+
AZTEC_EPOCH_DURATION,
|
|
13
|
+
ETHEREUM_SLOT_DURATION,
|
|
14
|
+
EthAddress,
|
|
15
|
+
type FeeRecipient,
|
|
16
|
+
type Header,
|
|
17
|
+
type Proof,
|
|
18
|
+
type RootRollupPublicInputs,
|
|
19
|
+
} from '@aztec/circuits.js';
|
|
4
20
|
import { createEthereumChain } from '@aztec/ethereum';
|
|
21
|
+
import { makeTuple } from '@aztec/foundation/array';
|
|
22
|
+
import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection';
|
|
5
23
|
import { type Signature } from '@aztec/foundation/eth-signature';
|
|
6
|
-
import {
|
|
24
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
7
25
|
import { createDebugLogger } from '@aztec/foundation/log';
|
|
8
|
-
import { serializeToBuffer } from '@aztec/foundation/serialize';
|
|
26
|
+
import { type Tuple, serializeToBuffer, toFriendlyJSON } from '@aztec/foundation/serialize';
|
|
9
27
|
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
10
28
|
import { Timer } from '@aztec/foundation/timer';
|
|
11
29
|
import { RollupAbi } from '@aztec/l1-artifacts';
|
|
12
30
|
import { type TelemetryClient } from '@aztec/telemetry-client';
|
|
13
31
|
|
|
14
32
|
import pick from 'lodash.pick';
|
|
33
|
+
import { inspect } from 'util';
|
|
15
34
|
import {
|
|
35
|
+
type BaseError,
|
|
36
|
+
type Chain,
|
|
37
|
+
type Client,
|
|
38
|
+
type ContractFunctionExecutionError,
|
|
16
39
|
ContractFunctionRevertedError,
|
|
17
40
|
type GetContractReturnType,
|
|
18
41
|
type Hex,
|
|
19
42
|
type HttpTransport,
|
|
20
43
|
type PrivateKeyAccount,
|
|
44
|
+
type PublicActions,
|
|
21
45
|
type PublicClient,
|
|
46
|
+
type PublicRpcSchema,
|
|
47
|
+
type WalletActions,
|
|
22
48
|
type WalletClient,
|
|
49
|
+
type WalletRpcSchema,
|
|
23
50
|
createPublicClient,
|
|
24
51
|
createWalletClient,
|
|
25
52
|
encodeFunctionData,
|
|
53
|
+
getAbiItem,
|
|
26
54
|
getAddress,
|
|
27
55
|
getContract,
|
|
28
56
|
hexToBytes,
|
|
29
57
|
http,
|
|
58
|
+
publicActions,
|
|
30
59
|
} from 'viem';
|
|
31
60
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
32
61
|
import type * as chains from 'viem/chains';
|
|
33
62
|
|
|
34
63
|
import { type PublisherConfig, type TxSenderConfig } from './config.js';
|
|
35
64
|
import { L1PublisherMetrics } from './l1-publisher-metrics.js';
|
|
36
|
-
import {
|
|
65
|
+
import { prettyLogViemError } from './utils.js';
|
|
37
66
|
|
|
38
67
|
/**
|
|
39
68
|
* Stats for a sent transaction.
|
|
40
69
|
*/
|
|
41
70
|
export type TransactionStats = {
|
|
71
|
+
/** Address of the sender. */
|
|
72
|
+
sender: string;
|
|
42
73
|
/** Hash of the transaction. */
|
|
43
74
|
transactionHash: string;
|
|
44
75
|
/** Size in bytes of the tx calldata */
|
|
@@ -54,17 +85,19 @@ export type MinimalTransactionReceipt = {
|
|
|
54
85
|
/** True if the tx was successful, false if reverted. */
|
|
55
86
|
status: boolean;
|
|
56
87
|
/** Hash of the transaction. */
|
|
57
|
-
transactionHash: string
|
|
88
|
+
transactionHash: `0x${string}`;
|
|
58
89
|
/** Effective gas used by the tx. */
|
|
59
90
|
gasUsed: bigint;
|
|
60
91
|
/** Effective gas price paid by the tx. */
|
|
61
92
|
gasPrice: bigint;
|
|
62
93
|
/** Logs emitted in this tx. */
|
|
63
94
|
logs: any[];
|
|
95
|
+
/** Block number in which this tx was mined. */
|
|
96
|
+
blockNumber: bigint;
|
|
64
97
|
};
|
|
65
98
|
|
|
66
99
|
/** Arguments to the process method of the rollup contract */
|
|
67
|
-
|
|
100
|
+
type L1ProcessArgs = {
|
|
68
101
|
/** The L2 block header. */
|
|
69
102
|
header: Buffer;
|
|
70
103
|
/** A root of the archive tree after the L2 block is applied. */
|
|
@@ -79,18 +112,18 @@ export type L1ProcessArgs = {
|
|
|
79
112
|
attestations?: Signature[];
|
|
80
113
|
};
|
|
81
114
|
|
|
82
|
-
/** Arguments to the
|
|
83
|
-
export type
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
/** Arguments to the submitEpochProof method of the rollup contract */
|
|
116
|
+
export type L1SubmitEpochProofArgs = {
|
|
117
|
+
epochSize: number;
|
|
118
|
+
previousArchive: Fr;
|
|
119
|
+
endArchive: Fr;
|
|
120
|
+
previousBlockHash: Fr;
|
|
121
|
+
endBlockHash: Fr;
|
|
122
|
+
endTimestamp: Fr;
|
|
123
|
+
outHash: Fr;
|
|
124
|
+
proverId: Fr;
|
|
125
|
+
fees: Tuple<FeeRecipient, typeof AZTEC_EPOCH_DURATION>;
|
|
126
|
+
proof: Proof;
|
|
94
127
|
};
|
|
95
128
|
|
|
96
129
|
/**
|
|
@@ -106,16 +139,20 @@ export class L1Publisher {
|
|
|
106
139
|
private sleepTimeMs: number;
|
|
107
140
|
private interrupted = false;
|
|
108
141
|
private metrics: L1PublisherMetrics;
|
|
109
|
-
|
|
142
|
+
|
|
143
|
+
protected log = createDebugLogger('aztec:sequencer:publisher');
|
|
110
144
|
|
|
111
145
|
private rollupContract: GetContractReturnType<
|
|
112
146
|
typeof RollupAbi,
|
|
113
147
|
WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>
|
|
114
148
|
>;
|
|
149
|
+
|
|
115
150
|
private publicClient: PublicClient<HttpTransport, chains.Chain>;
|
|
151
|
+
private walletClient: WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>;
|
|
116
152
|
private account: PrivateKeyAccount;
|
|
117
153
|
|
|
118
|
-
public static PROPOSE_GAS_GUESS: bigint =
|
|
154
|
+
public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
|
|
155
|
+
public static PROPOSE_AND_CLAIM_GAS_GUESS: bigint = this.PROPOSE_GAS_GUESS + 100_000n;
|
|
119
156
|
|
|
120
157
|
constructor(config: TxSenderConfig & PublisherConfig, client: TelemetryClient) {
|
|
121
158
|
this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000;
|
|
@@ -125,7 +162,8 @@ export class L1Publisher {
|
|
|
125
162
|
const chain = createEthereumChain(rpcUrl, chainId);
|
|
126
163
|
this.account = privateKeyToAccount(publisherPrivateKey);
|
|
127
164
|
this.log.debug(`Publishing from address ${this.account.address}`);
|
|
128
|
-
|
|
165
|
+
|
|
166
|
+
this.walletClient = createWalletClient({
|
|
129
167
|
account: this.account,
|
|
130
168
|
chain: chain.chainInfo,
|
|
131
169
|
transport: http(chain.rpcUrl),
|
|
@@ -134,17 +172,35 @@ export class L1Publisher {
|
|
|
134
172
|
this.publicClient = createPublicClient({
|
|
135
173
|
chain: chain.chainInfo,
|
|
136
174
|
transport: http(chain.rpcUrl),
|
|
175
|
+
pollingInterval: config.viemPollingIntervalMS,
|
|
137
176
|
});
|
|
138
177
|
|
|
139
178
|
this.rollupContract = getContract({
|
|
140
179
|
address: getAddress(l1Contracts.rollupAddress.toString()),
|
|
141
180
|
abi: RollupAbi,
|
|
142
|
-
client: walletClient,
|
|
181
|
+
client: this.walletClient,
|
|
143
182
|
});
|
|
144
183
|
}
|
|
145
184
|
|
|
146
|
-
public getSenderAddress():
|
|
147
|
-
return
|
|
185
|
+
public getSenderAddress(): EthAddress {
|
|
186
|
+
return EthAddress.fromString(this.account.address);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public getClient(): Client<
|
|
190
|
+
HttpTransport,
|
|
191
|
+
Chain,
|
|
192
|
+
PrivateKeyAccount,
|
|
193
|
+
[...WalletRpcSchema, ...PublicRpcSchema],
|
|
194
|
+
PublicActions<HttpTransport, Chain> & WalletActions<Chain, PrivateKeyAccount>
|
|
195
|
+
> {
|
|
196
|
+
return this.walletClient.extend(publicActions);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public getRollupContract(): GetContractReturnType<
|
|
200
|
+
typeof RollupAbi,
|
|
201
|
+
WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>
|
|
202
|
+
> {
|
|
203
|
+
return this.rollupContract;
|
|
148
204
|
}
|
|
149
205
|
|
|
150
206
|
/**
|
|
@@ -157,11 +213,69 @@ export class L1Publisher {
|
|
|
157
213
|
* @return blockNumber - The L2 block number of the next L2 block
|
|
158
214
|
*/
|
|
159
215
|
public async canProposeAtNextEthBlock(archive: Buffer): Promise<[bigint, bigint]> {
|
|
216
|
+
// FIXME: This should not throw if unable to propose but return a falsey value, so
|
|
217
|
+
// we can differentiate between errors when hitting the L1 rollup contract (eg RPC error)
|
|
218
|
+
// which may require a retry, vs actually not being the turn for proposing.
|
|
160
219
|
const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
|
|
161
220
|
const [slot, blockNumber] = await this.rollupContract.read.canProposeAtTime([ts, `0x${archive.toString('hex')}`]);
|
|
162
221
|
return [slot, blockNumber];
|
|
163
222
|
}
|
|
164
223
|
|
|
224
|
+
public async nextEpochToClaim(): Promise<bigint> {
|
|
225
|
+
return await this.rollupContract.read.nextEpochToClaim();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public async getEpochForSlotNumber(slotNumber: bigint): Promise<bigint> {
|
|
229
|
+
return await this.rollupContract.read.getEpochAtSlot([slotNumber]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public async getEpochToProve(): Promise<bigint | undefined> {
|
|
233
|
+
try {
|
|
234
|
+
return await this.rollupContract.read.getEpochToProve();
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
// If this is a revert with Rollup__NoEpochToProve, it means there is no epoch to prove, so we return undefined
|
|
237
|
+
// See https://viem.sh/docs/contract/simulateContract#handling-custom-errors
|
|
238
|
+
const errorName = tryGetCustomErrorName(err);
|
|
239
|
+
if (errorName === getAbiItem({ abi: RollupAbi, name: 'Rollup__NoEpochToProve' }).name) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public async getProofClaim(): Promise<EpochProofClaim | undefined> {
|
|
247
|
+
const [epochToProve, basisPointFee, bondAmount, bondProviderHex, proposerClaimantHex] =
|
|
248
|
+
await this.rollupContract.read.proofClaim();
|
|
249
|
+
|
|
250
|
+
const bondProvider = EthAddress.fromString(bondProviderHex);
|
|
251
|
+
const proposerClaimant = EthAddress.fromString(proposerClaimantHex);
|
|
252
|
+
|
|
253
|
+
if (bondProvider.isZero() && proposerClaimant.isZero() && epochToProve === 0n) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
epochToProve,
|
|
259
|
+
basisPointFee,
|
|
260
|
+
bondAmount,
|
|
261
|
+
bondProvider,
|
|
262
|
+
proposerClaimant,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public async validateProofQuote(quote: EpochProofQuote): Promise<EpochProofQuote | undefined> {
|
|
267
|
+
const args = [quote.toViemArgs()] as const;
|
|
268
|
+
try {
|
|
269
|
+
await this.rollupContract.read.validateEpochProofRightClaim(args, { account: this.account });
|
|
270
|
+
} catch (err) {
|
|
271
|
+
this.log.verbose(toFriendlyJSON(err as object));
|
|
272
|
+
const errorName = tryGetCustomErrorName(err);
|
|
273
|
+
this.log.warn(`Proof quote validation failed: ${errorName}`);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return quote;
|
|
277
|
+
}
|
|
278
|
+
|
|
165
279
|
/**
|
|
166
280
|
* @notice Will call `validateHeader` to make sure that it is possible to propose
|
|
167
281
|
*
|
|
@@ -218,6 +332,7 @@ export class L1Publisher {
|
|
|
218
332
|
}
|
|
219
333
|
const calldata = hexToBytes(tx.input);
|
|
220
334
|
return {
|
|
335
|
+
sender: tx.from.toString(),
|
|
221
336
|
transactionHash: tx.hash,
|
|
222
337
|
calldataSize: calldata.length,
|
|
223
338
|
calldataGas: getCalldataGasUsage(calldata),
|
|
@@ -229,7 +344,12 @@ export class L1Publisher {
|
|
|
229
344
|
* @param block - L2 block to propose.
|
|
230
345
|
* @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise.
|
|
231
346
|
*/
|
|
232
|
-
public async proposeL2Block(
|
|
347
|
+
public async proposeL2Block(
|
|
348
|
+
block: L2Block,
|
|
349
|
+
attestations?: Signature[],
|
|
350
|
+
txHashes?: TxHash[],
|
|
351
|
+
proofQuote?: EpochProofQuote,
|
|
352
|
+
): Promise<boolean> {
|
|
233
353
|
const ctx = {
|
|
234
354
|
blockNumber: block.number,
|
|
235
355
|
slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
|
|
@@ -249,77 +369,113 @@ export class L1Publisher {
|
|
|
249
369
|
};
|
|
250
370
|
|
|
251
371
|
// Publish body and propose block (if not already published)
|
|
252
|
-
if (
|
|
253
|
-
|
|
372
|
+
if (this.interrupted) {
|
|
373
|
+
this.log.verbose('L2 block data syncing interrupted while processing blocks.', ctx);
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
254
376
|
|
|
255
|
-
|
|
256
|
-
// This means that we can avoid the simulation issues in later checks.
|
|
257
|
-
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
258
|
-
// make time consistency checks break.
|
|
259
|
-
await this.validateBlockForSubmission(block.header, {
|
|
260
|
-
digest: digest.toBuffer(),
|
|
261
|
-
signatures: attestations ?? [],
|
|
262
|
-
});
|
|
377
|
+
const timer = new Timer();
|
|
263
378
|
|
|
264
|
-
|
|
379
|
+
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
380
|
+
// This means that we can avoid the simulation issues in later checks.
|
|
381
|
+
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
382
|
+
// make time consistency checks break.
|
|
383
|
+
await this.validateBlockForSubmission(block.header, {
|
|
384
|
+
digest: digest.toBuffer(),
|
|
385
|
+
signatures: attestations ?? [],
|
|
386
|
+
});
|
|
265
387
|
|
|
266
|
-
|
|
267
|
-
this.log.info(`Failed to publish block ${block.number} to L1`, ctx);
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
388
|
+
this.log.verbose(`Submitting propose transaction`);
|
|
270
389
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
390
|
+
const tx = proofQuote
|
|
391
|
+
? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote)
|
|
392
|
+
: await this.sendProposeTx(proposeTxArgs);
|
|
276
393
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
|
|
282
|
-
...pick(tx!, 'calldataGas', 'calldataSize'),
|
|
283
|
-
...block.getStats(),
|
|
284
|
-
eventName: 'rollup-published-to-l1',
|
|
285
|
-
};
|
|
286
|
-
this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx });
|
|
287
|
-
this.metrics.recordProcessBlockTx(timer.ms(), stats);
|
|
394
|
+
if (!tx) {
|
|
395
|
+
this.log.info(`Failed to publish block ${block.number} to L1`, ctx);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
288
398
|
|
|
289
|
-
|
|
290
|
-
}
|
|
399
|
+
const { hash: txHash, args, functionName, gasLimit } = tx;
|
|
291
400
|
|
|
292
|
-
|
|
401
|
+
const receipt = await this.getTransactionReceipt(txHash);
|
|
402
|
+
if (!receipt) {
|
|
403
|
+
this.log.info(`Failed to get receipt for tx ${txHash}`, ctx);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
293
406
|
|
|
294
|
-
|
|
295
|
-
|
|
407
|
+
// Tx was mined successfully
|
|
408
|
+
if (receipt.status) {
|
|
409
|
+
const tx = await this.getTransactionStats(txHash);
|
|
410
|
+
const stats: L1PublishBlockStats = {
|
|
411
|
+
...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
|
|
412
|
+
...pick(tx!, 'calldataGas', 'calldataSize', 'sender'),
|
|
413
|
+
...block.getStats(),
|
|
414
|
+
eventName: 'rollup-published-to-l1',
|
|
415
|
+
};
|
|
416
|
+
this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx });
|
|
417
|
+
this.metrics.recordProcessBlockTx(timer.ms(), stats);
|
|
418
|
+
|
|
419
|
+
return true;
|
|
296
420
|
}
|
|
297
421
|
|
|
298
|
-
this.
|
|
422
|
+
this.metrics.recordFailedTx('process');
|
|
423
|
+
|
|
424
|
+
const errorMsg = await this.tryGetErrorFromRevertedTx({
|
|
425
|
+
args,
|
|
426
|
+
functionName,
|
|
427
|
+
gasLimit,
|
|
428
|
+
abi: RollupAbi,
|
|
429
|
+
address: this.rollupContract.address,
|
|
430
|
+
blockNumber: receipt.blockNumber,
|
|
431
|
+
});
|
|
432
|
+
this.log.error(`Rollup process tx reverted. ${errorMsg}`, undefined, {
|
|
433
|
+
...ctx,
|
|
434
|
+
txHash: receipt.transactionHash,
|
|
435
|
+
});
|
|
436
|
+
await this.sleepOrInterrupted();
|
|
299
437
|
return false;
|
|
300
438
|
}
|
|
301
439
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
440
|
+
private async tryGetErrorFromRevertedTx(args: {
|
|
441
|
+
args: any[];
|
|
442
|
+
functionName: string;
|
|
443
|
+
gasLimit: bigint;
|
|
444
|
+
abi: any;
|
|
445
|
+
address: Hex;
|
|
446
|
+
blockNumber: bigint | undefined;
|
|
447
|
+
}) {
|
|
448
|
+
try {
|
|
449
|
+
await this.publicClient.simulateContract({ ...args, account: this.walletClient.account });
|
|
450
|
+
return undefined;
|
|
451
|
+
} catch (err: any) {
|
|
452
|
+
if (err.name === 'ContractFunctionExecutionError') {
|
|
453
|
+
const execErr = err as ContractFunctionExecutionError;
|
|
454
|
+
return compactArray([
|
|
455
|
+
execErr.shortMessage,
|
|
456
|
+
...(execErr.metaMessages ?? []).slice(0, 2).map(s => s.trim()),
|
|
457
|
+
]).join(' ');
|
|
458
|
+
}
|
|
459
|
+
this.log.error(`Error getting error from simulation`, err);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
318
462
|
|
|
319
|
-
|
|
463
|
+
public async submitEpochProof(args: {
|
|
464
|
+
epochNumber: number;
|
|
465
|
+
fromBlock: number;
|
|
466
|
+
toBlock: number;
|
|
467
|
+
publicInputs: RootRollupPublicInputs;
|
|
468
|
+
proof: Proof;
|
|
469
|
+
}): Promise<boolean> {
|
|
470
|
+
const { epochNumber, fromBlock, toBlock } = args;
|
|
471
|
+
const ctx = { epochNumber, fromBlock, toBlock };
|
|
320
472
|
if (!this.interrupted) {
|
|
321
473
|
const timer = new Timer();
|
|
322
|
-
|
|
474
|
+
|
|
475
|
+
// Validate epoch proof range and hashes are correct before submitting
|
|
476
|
+
await this.validateEpochProofSubmission(args);
|
|
477
|
+
|
|
478
|
+
const txHash = await this.sendSubmitEpochProofTx(args);
|
|
323
479
|
if (!txHash) {
|
|
324
480
|
return false;
|
|
325
481
|
}
|
|
@@ -334,16 +490,16 @@ export class L1Publisher {
|
|
|
334
490
|
const tx = await this.getTransactionStats(txHash);
|
|
335
491
|
const stats: L1PublishProofStats = {
|
|
336
492
|
...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
|
|
337
|
-
...pick(tx!, 'calldataGas', 'calldataSize'),
|
|
493
|
+
...pick(tx!, 'calldataGas', 'calldataSize', 'sender'),
|
|
338
494
|
eventName: 'proof-published-to-l1',
|
|
339
495
|
};
|
|
340
|
-
this.log.info(`Published proof to L1 rollup contract`, { ...stats, ...ctx });
|
|
496
|
+
this.log.info(`Published epoch proof to L1 rollup contract`, { ...stats, ...ctx });
|
|
341
497
|
this.metrics.recordSubmitProof(timer.ms(), stats);
|
|
342
498
|
return true;
|
|
343
499
|
}
|
|
344
500
|
|
|
345
501
|
this.metrics.recordFailedTx('submitProof');
|
|
346
|
-
this.log.error(`Rollup.
|
|
502
|
+
this.log.error(`Rollup.submitEpochProof tx status failed: ${receipt.transactionHash}`, ctx);
|
|
347
503
|
await this.sleepOrInterrupted();
|
|
348
504
|
}
|
|
349
505
|
|
|
@@ -351,6 +507,63 @@ export class L1Publisher {
|
|
|
351
507
|
return false;
|
|
352
508
|
}
|
|
353
509
|
|
|
510
|
+
private async validateEpochProofSubmission(args: {
|
|
511
|
+
fromBlock: number;
|
|
512
|
+
toBlock: number;
|
|
513
|
+
publicInputs: RootRollupPublicInputs;
|
|
514
|
+
proof: Proof;
|
|
515
|
+
}) {
|
|
516
|
+
const { fromBlock, toBlock, publicInputs, proof } = args;
|
|
517
|
+
|
|
518
|
+
// Check that the block numbers match the expected epoch to be proven
|
|
519
|
+
const [pending, proven] = await this.rollupContract.read.tips();
|
|
520
|
+
if (proven !== BigInt(fromBlock) - 1n) {
|
|
521
|
+
throw new Error(`Cannot submit epoch proof for ${fromBlock}-${toBlock} as proven block is ${proven}`);
|
|
522
|
+
}
|
|
523
|
+
if (toBlock > pending) {
|
|
524
|
+
throw new Error(`Cannot submit epoch proof for ${fromBlock}-${toBlock} as pending block is ${pending}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check the block hash and archive for the immediate block before the epoch
|
|
528
|
+
const [previousArchive, previousBlockHash] = await this.rollupContract.read.blocks([proven]);
|
|
529
|
+
if (publicInputs.previousArchive.root.toString() !== previousArchive) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Previous archive root mismatch: ${publicInputs.previousArchive.root.toString()} !== ${previousArchive}`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
// TODO: Remove zero check once we inject the proper zero blockhash
|
|
535
|
+
if (previousBlockHash !== Fr.ZERO.toString() && publicInputs.previousBlockHash.toString() !== previousBlockHash) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Previous block hash mismatch: ${publicInputs.previousBlockHash.toString()} !== ${previousBlockHash}`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Check the block hash and archive for the last block in the epoch
|
|
542
|
+
const [endArchive, endBlockHash] = await this.rollupContract.read.blocks([BigInt(toBlock)]);
|
|
543
|
+
if (publicInputs.endArchive.root.toString() !== endArchive) {
|
|
544
|
+
throw new Error(`End archive root mismatch: ${publicInputs.endArchive.root.toString()} !== ${endArchive}`);
|
|
545
|
+
}
|
|
546
|
+
if (publicInputs.endBlockHash.toString() !== endBlockHash) {
|
|
547
|
+
throw new Error(`End block hash mismatch: ${publicInputs.endBlockHash.toString()} !== ${endBlockHash}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Compare the public inputs computed by the contract with the ones injected
|
|
551
|
+
const rollupPublicInputs = await this.rollupContract.read.getEpochProofPublicInputs(
|
|
552
|
+
this.getSubmitEpochProofArgs(args),
|
|
553
|
+
);
|
|
554
|
+
const aggregationObject = proof.isEmpty()
|
|
555
|
+
? times(AGGREGATION_OBJECT_LENGTH, Fr.zero)
|
|
556
|
+
: proof.extractAggregationObject();
|
|
557
|
+
const argsPublicInputs = [...publicInputs.toFields(), ...aggregationObject];
|
|
558
|
+
|
|
559
|
+
if (!areArraysEqual(rollupPublicInputs.map(Fr.fromString), argsPublicInputs, (a, b) => a.equals(b))) {
|
|
560
|
+
const fmt = (inputs: Fr[] | readonly string[]) => inputs.map(x => x.toString()).join(', ');
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Root rollup public inputs mismatch:\nRollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
354
567
|
/**
|
|
355
568
|
* Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
|
|
356
569
|
* Be warned, the call may return false even if the tx subsequently gets successfully mined.
|
|
@@ -367,75 +580,137 @@ export class L1Publisher {
|
|
|
367
580
|
this.interrupted = false;
|
|
368
581
|
}
|
|
369
582
|
|
|
370
|
-
private async
|
|
583
|
+
private async sendSubmitEpochProofTx(args: {
|
|
584
|
+
fromBlock: number;
|
|
585
|
+
toBlock: number;
|
|
586
|
+
publicInputs: RootRollupPublicInputs;
|
|
587
|
+
proof: Proof;
|
|
588
|
+
}): Promise<string | undefined> {
|
|
371
589
|
try {
|
|
372
|
-
const
|
|
373
|
-
this.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
`0x${header.toString('hex')}`,
|
|
378
|
-
`0x${archive.toString('hex')}`,
|
|
379
|
-
`0x${proverId.toString('hex')}`,
|
|
380
|
-
`0x${aggregationObject.toString('hex')}`,
|
|
381
|
-
`0x${proof.toString('hex')}`,
|
|
382
|
-
] as const;
|
|
383
|
-
|
|
384
|
-
await this.rollupContract.simulate.submitBlockRootProof(args, {
|
|
385
|
-
account: this.account,
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
return await this.rollupContract.write.submitBlockRootProof(args, {
|
|
389
|
-
account: this.account,
|
|
390
|
-
});
|
|
590
|
+
const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`;
|
|
591
|
+
const txArgs = [...this.getSubmitEpochProofArgs(args), proofHex] as const;
|
|
592
|
+
this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`);
|
|
593
|
+
await this.rollupContract.simulate.submitEpochRootProof(txArgs, { account: this.account });
|
|
594
|
+
return await this.rollupContract.write.submitEpochRootProof(txArgs, { account: this.account });
|
|
391
595
|
} catch (err) {
|
|
392
|
-
this.log.error(`Rollup submit proof failed`, err);
|
|
596
|
+
this.log.error(`Rollup submit epoch proof failed`, err);
|
|
393
597
|
return undefined;
|
|
394
598
|
}
|
|
395
599
|
}
|
|
396
600
|
|
|
397
|
-
private async
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
601
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs, gasGuess: bigint) {
|
|
602
|
+
// We have to jump a few hoops because viem is not happy around estimating gas for view functions
|
|
603
|
+
const computeTxsEffectsHashGas = await this.publicClient.estimateGas({
|
|
604
|
+
to: this.rollupContract.address,
|
|
605
|
+
data: encodeFunctionData({
|
|
606
|
+
abi: this.rollupContract.abi,
|
|
607
|
+
functionName: 'computeTxsEffectsHash',
|
|
608
|
+
args: [`0x${encodedData.body.toString('hex')}`],
|
|
609
|
+
}),
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// @note We perform this guesstimate instead of the usual `gasEstimate` since
|
|
613
|
+
// viem will use the current state to simulate against, which means that
|
|
614
|
+
// we will fail estimation in the case where we are simulating for the
|
|
615
|
+
// first ethereum block within our slot (as current time is not in the
|
|
616
|
+
// slot yet).
|
|
617
|
+
const gasGuesstimate = computeTxsEffectsHashGas + gasGuess;
|
|
618
|
+
|
|
619
|
+
const attestations = encodedData.attestations
|
|
620
|
+
? encodedData.attestations.map(attest => attest.toViemSignature())
|
|
621
|
+
: [];
|
|
622
|
+
const txHashes = encodedData.txHashes ? encodedData.txHashes.map(txHash => txHash.to0xString()) : [];
|
|
623
|
+
const args = [
|
|
624
|
+
`0x${encodedData.header.toString('hex')}`,
|
|
625
|
+
`0x${encodedData.archive.toString('hex')}`,
|
|
626
|
+
`0x${encodedData.blockHash.toString('hex')}`,
|
|
627
|
+
txHashes,
|
|
628
|
+
attestations,
|
|
629
|
+
`0x${encodedData.body.toString('hex')}`,
|
|
630
|
+
] as const;
|
|
631
|
+
|
|
632
|
+
return { args, gasGuesstimate };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private getSubmitEpochProofArgs(args: {
|
|
636
|
+
fromBlock: number;
|
|
637
|
+
toBlock: number;
|
|
638
|
+
publicInputs: RootRollupPublicInputs;
|
|
639
|
+
proof: Proof;
|
|
640
|
+
}) {
|
|
641
|
+
return [
|
|
642
|
+
BigInt(args.toBlock - args.fromBlock + 1),
|
|
643
|
+
[
|
|
644
|
+
args.publicInputs.previousArchive.root.toString(),
|
|
645
|
+
args.publicInputs.endArchive.root.toString(),
|
|
646
|
+
args.publicInputs.previousBlockHash.toString(),
|
|
647
|
+
args.publicInputs.endBlockHash.toString(),
|
|
648
|
+
args.publicInputs.endTimestamp.toString(),
|
|
649
|
+
args.publicInputs.outHash.toString(),
|
|
650
|
+
args.publicInputs.proverId.toString(),
|
|
651
|
+
],
|
|
652
|
+
makeTuple(AZTEC_EPOCH_DURATION * 2, i =>
|
|
653
|
+
i % 2 === 0
|
|
654
|
+
? args.publicInputs.fees[i / 2].recipient.toField().toString()
|
|
655
|
+
: args.publicInputs.fees[(i - 1) / 2].value.toString(),
|
|
656
|
+
),
|
|
657
|
+
`0x${serializeToBuffer(args.proof.extractAggregationObject()).toString('hex')}`,
|
|
658
|
+
] as const;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private async sendProposeTx(
|
|
662
|
+
encodedData: L1ProcessArgs,
|
|
663
|
+
): Promise<{ hash: string; args: any; functionName: string; gasLimit: bigint } | undefined> {
|
|
664
|
+
if (this.interrupted) {
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const { args, gasGuesstimate } = await this.prepareProposeTx(encodedData, L1Publisher.PROPOSE_GAS_GUESS);
|
|
409
669
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// we will fail estimation in the case where we are simulating for the
|
|
413
|
-
// first ethereum block within our slot (as current time is not in the
|
|
414
|
-
// slot yet).
|
|
415
|
-
const gasGuesstimate = computeTxsEffectsHashGas + L1Publisher.PROPOSE_GAS_GUESS;
|
|
416
|
-
|
|
417
|
-
const attestations = encodedData.attestations
|
|
418
|
-
? encodedData.attestations.map(attest => attest.toViemSignature())
|
|
419
|
-
: [];
|
|
420
|
-
const txHashes = encodedData.txHashes ? encodedData.txHashes.map(txHash => txHash.to0xString()) : [];
|
|
421
|
-
const args = [
|
|
422
|
-
`0x${encodedData.header.toString('hex')}`,
|
|
423
|
-
`0x${encodedData.archive.toString('hex')}`,
|
|
424
|
-
`0x${encodedData.blockHash.toString('hex')}`,
|
|
425
|
-
txHashes,
|
|
426
|
-
attestations,
|
|
427
|
-
`0x${encodedData.body.toString('hex')}`,
|
|
428
|
-
] as const;
|
|
429
|
-
|
|
430
|
-
return await this.rollupContract.write.propose(args, {
|
|
670
|
+
return {
|
|
671
|
+
hash: await this.rollupContract.write.propose(args, {
|
|
431
672
|
account: this.account,
|
|
432
673
|
gas: gasGuesstimate,
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
674
|
+
}),
|
|
675
|
+
args,
|
|
676
|
+
functionName: 'propose',
|
|
677
|
+
gasLimit: gasGuesstimate,
|
|
678
|
+
};
|
|
679
|
+
} catch (err) {
|
|
680
|
+
prettyLogViemError(err, this.log);
|
|
681
|
+
this.log.error(`Rollup publish failed`, err);
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private async sendProposeAndClaimTx(
|
|
687
|
+
encodedData: L1ProcessArgs,
|
|
688
|
+
quote: EpochProofQuote,
|
|
689
|
+
): Promise<{ hash: string; args: any; functionName: string; gasLimit: bigint } | undefined> {
|
|
690
|
+
if (this.interrupted) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const { args, gasGuesstimate } = await this.prepareProposeTx(
|
|
695
|
+
encodedData,
|
|
696
|
+
L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS,
|
|
697
|
+
);
|
|
698
|
+
this.log.info(`ProposeAndClaim`);
|
|
699
|
+
this.log.info(inspect(quote.payload));
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
hash: await this.rollupContract.write.proposeAndClaim([...args, quote.toViemArgs()], {
|
|
703
|
+
account: this.account,
|
|
704
|
+
gas: gasGuesstimate,
|
|
705
|
+
}),
|
|
706
|
+
functionName: 'proposeAndClaim',
|
|
707
|
+
args,
|
|
708
|
+
gasLimit: gasGuesstimate,
|
|
709
|
+
};
|
|
710
|
+
} catch (err) {
|
|
711
|
+
prettyLogViemError(err, this.log);
|
|
712
|
+
this.log.error(`Rollup publish failed`, err);
|
|
713
|
+
return undefined;
|
|
439
714
|
}
|
|
440
715
|
}
|
|
441
716
|
|
|
@@ -462,6 +737,7 @@ export class L1Publisher {
|
|
|
462
737
|
gasUsed: receipt.gasUsed,
|
|
463
738
|
gasPrice: receipt.effectiveGasPrice,
|
|
464
739
|
logs: receipt.logs,
|
|
740
|
+
blockNumber: receipt.blockNumber,
|
|
465
741
|
};
|
|
466
742
|
}
|
|
467
743
|
|
|
@@ -487,3 +763,18 @@ export class L1Publisher {
|
|
|
487
763
|
function getCalldataGasUsage(data: Uint8Array) {
|
|
488
764
|
return data.filter(byte => byte === 0).length * 4 + data.filter(byte => byte !== 0).length * 16;
|
|
489
765
|
}
|
|
766
|
+
|
|
767
|
+
function tryGetCustomErrorName(err: any) {
|
|
768
|
+
try {
|
|
769
|
+
// See https://viem.sh/docs/contract/simulateContract#handling-custom-errors
|
|
770
|
+
if (err.name === 'ViemError') {
|
|
771
|
+
const baseError = err as BaseError;
|
|
772
|
+
const revertError = baseError.walk(err => (err as Error).name === 'ContractFunctionRevertedError');
|
|
773
|
+
if (revertError) {
|
|
774
|
+
return (revertError as ContractFunctionRevertedError).data?.errorName;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} catch (_e) {
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
}
|