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