@aztec/sequencer-client 0.51.0 → 0.51.1

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.
@@ -1,8 +1,8 @@
1
1
  import { type L2Block, type Signature } from '@aztec/circuit-types';
2
2
  import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats';
3
- import { ETHEREUM_SLOT_DURATION, EthAddress, type Header, type Proof } from '@aztec/circuits.js';
3
+ import { ETHEREUM_SLOT_DURATION, EthAddress, GENESIS_ARCHIVE_ROOT, type Header, type Proof } from '@aztec/circuits.js';
4
4
  import { createEthereumChain } from '@aztec/ethereum';
5
- import { type Fr } from '@aztec/foundation/fields';
5
+ import { Fr } from '@aztec/foundation/fields';
6
6
  import { createDebugLogger } from '@aztec/foundation/log';
7
7
  import { serializeToBuffer } from '@aztec/foundation/serialize';
8
8
  import { InterruptibleSleep } from '@aztec/foundation/sleep';
@@ -12,6 +12,7 @@ import { type TelemetryClient } from '@aztec/telemetry-client';
12
12
 
13
13
  import pick from 'lodash.pick';
14
14
  import {
15
+ ContractFunctionRevertedError,
15
16
  type GetContractReturnType,
16
17
  type Hex,
17
18
  type HttpTransport,
@@ -59,13 +60,6 @@ export type MinimalTransactionReceipt = {
59
60
  logs: any[];
60
61
  };
61
62
 
62
- /**
63
- * @notice An attestation for the sequencing model.
64
- * @todo This is not where it belongs. But I think we should do a bigger rewrite of some of
65
- * this spaghetti.
66
- */
67
- export type Attestation = { isEmpty: boolean; v: number; r: `0x${string}`; s: `0x${string}` };
68
-
69
63
  /** Arguments to the process method of the rollup contract */
70
64
  export type L1ProcessArgs = {
71
65
  /** The L2 block header. */
@@ -94,6 +88,13 @@ export type L1SubmitProofArgs = {
94
88
  aggregationObject: Buffer;
95
89
  };
96
90
 
91
+ export type MetadataForSlot = {
92
+ proposer: EthAddress;
93
+ slot: bigint;
94
+ pendingBlockNumber: bigint;
95
+ archive: Buffer;
96
+ };
97
+
97
98
  /**
98
99
  * Publishes L2 blocks to L1. This implementation does *not* retry a transaction in
99
100
  * the event of network congestion, but should work for local development.
@@ -103,6 +104,15 @@ export type L1SubmitProofArgs = {
103
104
  * Adapted from https://github.com/AztecProtocol/aztec2-internal/blob/master/falafel/src/rollup_publisher.ts.
104
105
  */
105
106
  export class L1Publisher {
107
+ // @note If we want to simulate in the future, we have to skip the viem simulations and use `reads` instead
108
+ // This is because the viem simulations are not able to simulate the future, only the current state.
109
+ // This means that we will be simulating as if `block.timestamp` is the same for the next block
110
+ // as for the last block.
111
+ // Nevertheless, it can be quite useful for figuring out why exactly the transaction is failing
112
+ // as a middle ground right now, we will be skipping the simulation and just sending the transaction
113
+ // but only after we have done a successful run of the `validateHeader` for the timestamp in the future.
114
+ public static SKIP_SIMULATION = true;
115
+
106
116
  private interruptibleSleep = new InterruptibleSleep();
107
117
  private sleepTimeMs: number;
108
118
  private interrupted = false;
@@ -154,24 +164,76 @@ export class L1Publisher {
154
164
  return Promise.resolve(EthAddress.fromString(this.account.address));
155
165
  }
156
166
 
157
- // Computes who will be the L2 proposer at the next Ethereum block
158
- // Using next Ethereum block so we do NOT need to wait for it being mined before seeing the effect
159
- // @note Assumes that all ethereum slots have blocks
160
- async getProposerAtNextEthBlock(): Promise<EthAddress> {
167
+ /**
168
+ * @notice Calls `canProposeAtTime` with the time of the next Ethereum block and the sender address
169
+ *
170
+ * @dev Throws if unable to propose
171
+ *
172
+ * @param archive - The archive that we expect to be current state
173
+ * @return slot - The L2 slot number of the next Ethereum block,
174
+ * @return blockNumber - The L2 block number of the next L2 block
175
+ */
176
+ public async canProposeAtNextEthBlock(archive: Buffer): Promise<[bigint, bigint]> {
177
+ const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
178
+ const [slot, blockNumber] = await this.rollupContract.read.canProposeAtTime([
179
+ ts,
180
+ this.account.address,
181
+ `0x${archive.toString('hex')}`,
182
+ ]);
183
+ return [slot, blockNumber];
184
+ }
185
+
186
+ public async validateBlockForSubmission(
187
+ header: Header,
188
+ digest: Buffer = new Fr(GENESIS_ARCHIVE_ROOT).toBuffer(),
189
+ attestations: Signature[] = [],
190
+ ): Promise<boolean> {
191
+ const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
192
+
193
+ const formattedAttestations = attestations.map(attest => attest.toViemSignature());
194
+ const flags = { ignoreDA: true, ignoreSignatures: attestations.length == 0 };
195
+
196
+ const args = [
197
+ `0x${header.toBuffer().toString('hex')}`,
198
+ formattedAttestations,
199
+ `0x${digest.toString('hex')}`,
200
+ ts,
201
+ flags,
202
+ ] as const;
203
+
161
204
  try {
162
- const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
163
- const submitter = await this.rollupContract.read.getProposerAt([ts]);
164
- return EthAddress.fromString(submitter);
165
- } catch (err) {
166
- this.log.warn(`Failed to get submitter: ${err}`);
167
- return EthAddress.ZERO;
205
+ await this.rollupContract.read.validateHeader(args, { account: this.account });
206
+ return true;
207
+ } catch (error: unknown) {
208
+ // Specify the type of error
209
+ if (error instanceof ContractFunctionRevertedError) {
210
+ const err = error as ContractFunctionRevertedError;
211
+ this.log.debug(`Validation failed: ${err.message}`, err.data);
212
+ } else {
213
+ this.log.debug(`Unexpected error during validation: ${error}`);
214
+ }
215
+ return false;
168
216
  }
169
217
  }
170
218
 
171
- public async isItMyTurnToSubmit(): Promise<boolean> {
172
- const submitter = await this.getProposerAtNextEthBlock();
173
- const sender = await this.getSenderAddress();
174
- return submitter.isZero() || submitter.equals(sender);
219
+ // @note Assumes that all ethereum slots have blocks
220
+ // Using next Ethereum block so we do NOT need to wait for it being mined before seeing the effect
221
+ public async getMetadataForSlotAtNextEthBlock(): Promise<MetadataForSlot> {
222
+ const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
223
+
224
+ const [submitter, slot, pendingBlockCount, archive] = await Promise.all([
225
+ this.rollupContract.read.getProposerAt([ts]),
226
+ this.rollupContract.read.getSlotAt([ts]),
227
+ this.rollupContract.read.pendingBlockCount(),
228
+ this.rollupContract.read.archive(),
229
+ ]);
230
+
231
+ return {
232
+ proposer: EthAddress.fromString(submitter),
233
+ slot,
234
+ pendingBlockNumber: pendingBlockCount - 1n,
235
+ archive: Buffer.from(archive.replace('0x', ''), 'hex'),
236
+ };
175
237
  }
176
238
 
177
239
  public async getCurrentEpochCommittee(): Promise<EthAddress[]> {
@@ -203,44 +265,49 @@ export class L1Publisher {
203
265
  * @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise.
204
266
  */
205
267
  public async processL2Block(block: L2Block, attestations?: Signature[]): Promise<boolean> {
206
- const ctx = { blockNumber: block.number, blockHash: block.hash().toString() };
207
- // TODO(#4148) Remove this block number check, it's here because we don't currently have proper genesis state on the contract
208
- const lastArchive = block.header.lastArchive.root.toBuffer();
209
- if (block.number != 1 && !(await this.checkLastArchiveHash(lastArchive))) {
210
- this.log.info(`Detected different last archive prior to publishing a block, aborting publish...`, ctx);
268
+ const ctx = {
269
+ blockNumber: block.number,
270
+ slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
271
+ blockHash: block.hash().toString(),
272
+ };
273
+
274
+ // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
275
+ // This means that we can avoid the simulation issues in later checks.
276
+ // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
277
+ // make time consistency checks break.
278
+ if (!(await this.validateBlockForSubmission(block.header, block.archive.root.toBuffer(), attestations))) {
211
279
  return false;
212
280
  }
213
- const encodedBody = block.body.toBuffer();
214
281
 
215
282
  const processTxArgs = {
216
283
  header: block.header.toBuffer(),
217
284
  archive: block.archive.root.toBuffer(),
218
285
  blockHash: block.header.hash().toBuffer(),
219
- body: encodedBody,
286
+ body: block.body.toBuffer(),
220
287
  attestations,
221
288
  };
222
289
 
223
- // Process block and publish the body if needed (if not already published)
224
- while (!this.interrupted) {
290
+ // Publish body and propose block (if not already published)
291
+ if (!this.interrupted) {
225
292
  let txHash;
226
293
  const timer = new Timer();
227
294
 
228
295
  if (await this.checkIfTxsAreAvailable(block)) {
229
296
  this.log.verbose(`Transaction effects of block ${block.number} already published.`, ctx);
230
- txHash = await this.sendProcessTx(processTxArgs);
297
+ txHash = await this.sendProposeWithoutBodyTx(processTxArgs);
231
298
  } else {
232
- txHash = await this.sendPublishAndProcessTx(processTxArgs);
299
+ txHash = await this.sendProposeTx(processTxArgs);
233
300
  }
234
301
 
235
302
  if (!txHash) {
236
303
  this.log.info(`Failed to publish block ${block.number} to L1`, ctx);
237
- break;
304
+ return false;
238
305
  }
239
306
 
240
307
  const receipt = await this.getTransactionReceipt(txHash);
241
308
  if (!receipt) {
242
309
  this.log.info(`Failed to get receipt for tx ${txHash}`, ctx);
243
- break;
310
+ return false;
244
311
  }
245
312
 
246
313
  // Tx was mined successfully
@@ -254,17 +321,12 @@ export class L1Publisher {
254
321
  };
255
322
  this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx });
256
323
  this.metrics.recordProcessBlockTx(timer.ms(), stats);
324
+
257
325
  return true;
258
326
  }
259
327
 
260
328
  this.metrics.recordFailedTx('process');
261
329
 
262
- // Check if someone else incremented the block number
263
- if (!(await this.checkLastArchiveHash(lastArchive))) {
264
- this.log.warn('Publish failed. Detected different last archive hash.', ctx);
265
- break;
266
- }
267
-
268
330
  this.log.error(`Rollup.process tx status failed: ${receipt.transactionHash}`, ctx);
269
331
  await this.sleepOrInterrupted();
270
332
  }
@@ -280,7 +342,7 @@ export class L1Publisher {
280
342
  aggregationObject: Fr[],
281
343
  proof: Proof,
282
344
  ): Promise<boolean> {
283
- const ctx = { blockNumber: header.globalVariables.blockNumber };
345
+ const ctx = { blockNumber: header.globalVariables.blockNumber, slotNumber: header.globalVariables.slotNumber };
284
346
 
285
347
  const txArgs: L1SubmitProofArgs = {
286
348
  header: header.toBuffer(),
@@ -291,16 +353,16 @@ export class L1Publisher {
291
353
  };
292
354
 
293
355
  // Process block
294
- while (!this.interrupted) {
356
+ if (!this.interrupted) {
295
357
  const timer = new Timer();
296
358
  const txHash = await this.sendSubmitProofTx(txArgs);
297
359
  if (!txHash) {
298
- break;
360
+ return false;
299
361
  }
300
362
 
301
363
  const receipt = await this.getTransactionReceipt(txHash);
302
364
  if (!receipt) {
303
- break;
365
+ return false;
304
366
  }
305
367
 
306
368
  // Tx was mined successfully
@@ -341,26 +403,6 @@ export class L1Publisher {
341
403
  this.interrupted = false;
342
404
  }
343
405
 
344
- async getCurrentArchive(): Promise<Buffer> {
345
- const archive = await this.rollupContract.read.archive();
346
- return Buffer.from(archive.replace('0x', ''), 'hex');
347
- }
348
-
349
- /**
350
- * Verifies that the given value of last archive in a block header equals current archive of the rollup contract
351
- * @param lastArchive - The last archive of the block we wish to publish.
352
- * @returns Boolean indicating if the hashes are equal.
353
- */
354
- private async checkLastArchiveHash(lastArchive: Buffer): Promise<boolean> {
355
- const fromChain = await this.getCurrentArchive();
356
- const areSame = lastArchive.equals(fromChain);
357
- if (!areSame) {
358
- this.log.debug(`Contract archive: ${fromChain.toString('hex')}`);
359
- this.log.debug(`New block last archive: ${lastArchive.toString('hex')}`);
360
- }
361
- return areSame;
362
- }
363
-
364
406
  private async sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise<string | undefined> {
365
407
  try {
366
408
  const size = Object.values(submitProofArgs).reduce((acc, arg) => acc + arg.length, 0);
@@ -375,6 +417,12 @@ export class L1Publisher {
375
417
  `0x${proof.toString('hex')}`,
376
418
  ] as const;
377
419
 
420
+ if (!L1Publisher.SKIP_SIMULATION) {
421
+ await this.rollupContract.simulate.submitBlockRootProof(args, {
422
+ account: this.account,
423
+ });
424
+ }
425
+
378
426
  return await this.rollupContract.write.submitBlockRootProof(args, {
379
427
  account: this.account,
380
428
  });
@@ -384,12 +432,17 @@ export class L1Publisher {
384
432
  }
385
433
  }
386
434
 
435
+ // This is used in `integration_l1_publisher.test.ts` currently. Could be removed though.
387
436
  private async sendPublishTx(encodedBody: Buffer): Promise<string | undefined> {
388
- while (!this.interrupted) {
437
+ if (!this.interrupted) {
389
438
  try {
390
439
  this.log.info(`TxEffects size=${encodedBody.length} bytes`);
391
440
  const args = [`0x${encodedBody.toString('hex')}`] as const;
392
441
 
442
+ await this.availabilityOracleContract.simulate.publish(args, {
443
+ account: this.account,
444
+ });
445
+
393
446
  return await this.availabilityOracleContract.write.publish(args, {
394
447
  account: this.account,
395
448
  });
@@ -400,8 +453,8 @@ export class L1Publisher {
400
453
  }
401
454
  }
402
455
 
403
- private async sendProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
404
- while (!this.interrupted) {
456
+ private async sendProposeWithoutBodyTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
457
+ if (!this.interrupted) {
405
458
  try {
406
459
  if (encodedData.attestations) {
407
460
  const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
@@ -412,7 +465,11 @@ export class L1Publisher {
412
465
  attestations,
413
466
  ] as const;
414
467
 
415
- return await this.rollupContract.write.process(args, {
468
+ if (!L1Publisher.SKIP_SIMULATION) {
469
+ await this.rollupContract.simulate.propose(args, { account: this.account });
470
+ }
471
+
472
+ return await this.rollupContract.write.propose(args, {
416
473
  account: this.account,
417
474
  });
418
475
  } else {
@@ -422,7 +479,10 @@ export class L1Publisher {
422
479
  `0x${encodedData.blockHash.toString('hex')}`,
423
480
  ] as const;
424
481
 
425
- return await this.rollupContract.write.process(args, {
482
+ if (!L1Publisher.SKIP_SIMULATION) {
483
+ await this.rollupContract.simulate.propose(args, { account: this.account });
484
+ }
485
+ return await this.rollupContract.write.propose(args, {
426
486
  account: this.account,
427
487
  });
428
488
  }
@@ -433,10 +493,9 @@ export class L1Publisher {
433
493
  }
434
494
  }
435
495
 
436
- private async sendPublishAndProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
437
- while (!this.interrupted) {
496
+ private async sendProposeTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
497
+ if (!this.interrupted) {
438
498
  try {
439
- // @note This is quite a sin, but I'm committing war crimes in this code already.
440
499
  if (encodedData.attestations) {
441
500
  const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
442
501
  const args = [
@@ -447,7 +506,13 @@ export class L1Publisher {
447
506
  `0x${encodedData.body.toString('hex')}`,
448
507
  ] as const;
449
508
 
450
- return await this.rollupContract.write.publishAndProcess(args, {
509
+ if (!L1Publisher.SKIP_SIMULATION) {
510
+ await this.rollupContract.simulate.propose(args, {
511
+ account: this.account,
512
+ });
513
+ }
514
+
515
+ return await this.rollupContract.write.propose(args, {
451
516
  account: this.account,
452
517
  });
453
518
  } else {
@@ -458,7 +523,13 @@ export class L1Publisher {
458
523
  `0x${encodedData.body.toString('hex')}`,
459
524
  ] as const;
460
525
 
461
- return await this.rollupContract.write.publishAndProcess(args, {
526
+ if (!L1Publisher.SKIP_SIMULATION) {
527
+ await this.rollupContract.simulate.propose(args, {
528
+ account: this.account,
529
+ });
530
+ }
531
+
532
+ return await this.rollupContract.write.propose(args, {
462
533
  account: this.account,
463
534
  });
464
535
  }