@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.
Files changed (54) hide show
  1. package/dest/block_builder/index.d.ts +2 -2
  2. package/dest/block_builder/index.d.ts.map +1 -1
  3. package/dest/block_builder/light.d.ts +8 -12
  4. package/dest/block_builder/light.d.ts.map +1 -1
  5. package/dest/block_builder/light.js +11 -15
  6. package/dest/block_builder/orchestrator.d.ts +8 -11
  7. package/dest/block_builder/orchestrator.d.ts.map +1 -1
  8. package/dest/block_builder/orchestrator.js +3 -10
  9. package/dest/client/sequencer-client.d.ts +2 -2
  10. package/dest/client/sequencer-client.d.ts.map +1 -1
  11. package/dest/client/sequencer-client.js +3 -4
  12. package/dest/config.d.ts.map +1 -1
  13. package/dest/config.js +11 -9
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +2 -1
  16. package/dest/publisher/config.d.ts.map +1 -1
  17. package/dest/publisher/config.js +7 -2
  18. package/dest/publisher/index.d.ts +1 -1
  19. package/dest/publisher/index.d.ts.map +1 -1
  20. package/dest/publisher/index.js +1 -1
  21. package/dest/publisher/l1-publisher-metrics.d.ts.map +1 -1
  22. package/dest/publisher/l1-publisher-metrics.js +2 -1
  23. package/dest/publisher/l1-publisher.d.ts +52 -36
  24. package/dest/publisher/l1-publisher.d.ts.map +1 -1
  25. package/dest/publisher/l1-publisher.js +305 -117
  26. package/dest/publisher/utils.d.ts +1 -1
  27. package/dest/publisher/utils.js +1 -1
  28. package/dest/sequencer/sequencer.d.ts +6 -6
  29. package/dest/sequencer/sequencer.d.ts.map +1 -1
  30. package/dest/sequencer/sequencer.js +142 -88
  31. package/dest/tx_validator/gas_validator.d.ts.map +1 -1
  32. package/dest/tx_validator/gas_validator.js +7 -5
  33. package/dest/tx_validator/phases_validator.d.ts +1 -1
  34. package/dest/tx_validator/phases_validator.d.ts.map +1 -1
  35. package/dest/tx_validator/phases_validator.js +1 -1
  36. package/dest/tx_validator/tx_validator_factory.d.ts +9 -7
  37. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  38. package/dest/tx_validator/tx_validator_factory.js +20 -10
  39. package/package.json +22 -19
  40. package/src/block_builder/index.ts +2 -2
  41. package/src/block_builder/light.ts +16 -24
  42. package/src/block_builder/orchestrator.ts +10 -18
  43. package/src/client/sequencer-client.ts +3 -9
  44. package/src/config.ts +10 -8
  45. package/src/global_variable_builder/global_builder.ts +1 -0
  46. package/src/publisher/config.ts +6 -1
  47. package/src/publisher/index.ts +1 -1
  48. package/src/publisher/l1-publisher-metrics.ts +1 -0
  49. package/src/publisher/l1-publisher.ts +435 -144
  50. package/src/publisher/utils.ts +1 -1
  51. package/src/sequencer/sequencer.ts +173 -108
  52. package/src/tx_validator/gas_validator.ts +6 -7
  53. package/src/tx_validator/phases_validator.ts +1 -1
  54. package/src/tx_validator/tx_validator_factory.ts +39 -15
@@ -1,44 +1,75 @@
1
- import { ConsensusPayload, type L2Block, type TxHash, getHashedSignaturePayload } from '@aztec/circuit-types';
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 { ETHEREUM_SLOT_DURATION, EthAddress, type Header, type Proof } from '@aztec/circuits.js';
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 { type Fr } from '@aztec/foundation/fields';
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 { prettyLogVeimError } from './utils.js';
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
- export type L1ProcessArgs = {
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 submitProof method of the rollup contract */
83
- export type L1SubmitProofArgs = {
84
- /** The L2 block header. */
85
- header: Buffer;
86
- /** A root of the archive tree after the L2 block is applied. */
87
- archive: Buffer;
88
- /** Identifier of the prover. */
89
- proverId: Buffer;
90
- /** The proof for the block. */
91
- proof: Buffer;
92
- /** The aggregation object for the block's proof. */
93
- aggregationObject: Buffer;
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
- private log = createDebugLogger('aztec:sequencer:publisher');
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 = 500_000n;
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
- const walletClient = createWalletClient({
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(): Promise<EthAddress> {
147
- return Promise.resolve(EthAddress.fromString(this.account.address));
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(block: L2Block, attestations?: Signature[], txHashes?: TxHash[]): Promise<boolean> {
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 (!this.interrupted) {
253
- const timer = new Timer();
372
+ if (this.interrupted) {
373
+ this.log.verbose('L2 block data syncing interrupted while processing blocks.', ctx);
374
+ return false;
375
+ }
254
376
 
255
- // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
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
- const txHash = await this.sendProposeTx(proposeTxArgs);
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
- if (!txHash) {
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
- const receipt = await this.getTransactionReceipt(txHash);
272
- if (!receipt) {
273
- this.log.info(`Failed to get receipt for tx ${txHash}`, ctx);
274
- return false;
275
- }
390
+ const tx = proofQuote
391
+ ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote)
392
+ : await this.sendProposeTx(proposeTxArgs);
276
393
 
277
- // Tx was mined successfully
278
- if (receipt.status) {
279
- const tx = await this.getTransactionStats(txHash);
280
- const stats: L1PublishBlockStats = {
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
- return true;
290
- }
399
+ const { hash: txHash, args, functionName, gasLimit } = tx;
291
400
 
292
- this.metrics.recordFailedTx('process');
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
- this.log.error(`Rollup.process tx status failed: ${receipt.transactionHash}`, ctx);
295
- await this.sleepOrInterrupted();
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.log.verbose('L2 block data syncing interrupted while processing blocks.', ctx);
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
- public async submitProof(
303
- header: Header,
304
- archiveRoot: Fr,
305
- proverId: Fr,
306
- aggregationObject: Fr[],
307
- proof: Proof,
308
- ): Promise<boolean> {
309
- const ctx = { blockNumber: header.globalVariables.blockNumber, slotNumber: header.globalVariables.slotNumber };
310
-
311
- const txArgs: L1SubmitProofArgs = {
312
- header: header.toBuffer(),
313
- archive: archiveRoot.toBuffer(),
314
- proverId: proverId.toBuffer(),
315
- aggregationObject: serializeToBuffer(aggregationObject),
316
- proof: proof.withoutPublicInputs(),
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
- // Process block
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
- const txHash = await this.sendSubmitProofTx(txArgs);
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.submitProof tx status failed: ${receipt.transactionHash}`, ctx);
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 sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise<string | undefined> {
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 size = Object.values(submitProofArgs).reduce((acc, arg) => acc + arg.length, 0);
373
- this.log.info(`SubmitProof size=${size} bytes`);
374
-
375
- const { header, archive, proverId, aggregationObject, proof } = submitProofArgs;
376
- const args = [
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 sendProposeTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
398
- if (!this.interrupted) {
399
- try {
400
- // We have to jump a few hoops because viem is not happy around estimating gas for view functions
401
- const computeTxsEffectsHashGas = await this.publicClient.estimateGas({
402
- to: this.rollupContract.address,
403
- data: encodeFunctionData({
404
- abi: this.rollupContract.abi,
405
- functionName: 'computeTxsEffectsHash',
406
- args: [`0x${encodedData.body.toString('hex')}`],
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
- // @note We perform this guesstimate instead of the usual `gasEstimate` since
411
- // viem will use the current state to simulate against, which means that
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
- } catch (err) {
435
- prettyLogVeimError(err, this.log);
436
- this.log.error(`Rollup publish failed`, err);
437
- return undefined;
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
+ }