@aztec/sequencer-client 0.51.0 → 0.52.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.
@@ -52,23 +52,26 @@ export class GlobalVariableBuilder {
52
52
  * @param blockNumber - The block number to build global variables for.
53
53
  * @param coinbase - The address to receive block reward.
54
54
  * @param feeRecipient - The address to receive fees.
55
+ * @param slotNumber - The slot number to use for the global variables, if undefined it will be calculated.
55
56
  * @returns The global variables for the given block number.
56
57
  */
57
58
  public async buildGlobalVariables(
58
59
  blockNumber: Fr,
59
60
  coinbase: EthAddress,
60
61
  feeRecipient: AztecAddress,
62
+ slotNumber?: bigint,
61
63
  ): Promise<GlobalVariables> {
62
64
  const version = new Fr(await this.rollupContract.read.VERSION());
63
65
  const chainId = new Fr(this.publicClient.chain.id);
64
66
 
65
- const ts = (await this.publicClient.getBlock()).timestamp;
67
+ if (slotNumber === undefined) {
68
+ const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
69
+ slotNumber = await this.rollupContract.read.getSlotAt([ts]);
70
+ }
66
71
 
67
- // Not just the current slot, the slot of the next block.
68
- const slot = await this.rollupContract.read.getSlotAt([ts + BigInt(ETHEREUM_SLOT_DURATION)]);
69
- const timestamp = await this.rollupContract.read.getTimestampForSlot([slot]);
72
+ const timestamp = await this.rollupContract.read.getTimestampForSlot([slotNumber]);
70
73
 
71
- const slotFr = new Fr(slot);
74
+ const slotFr = new Fr(slotNumber);
72
75
  const timestampFr = new Fr(timestamp);
73
76
 
74
77
  const gasFees = GasFees.default();
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ export * from './publisher/index.js';
4
4
  export * from './sequencer/index.js';
5
5
  export * from './tx_validator/aggregate_tx_validator.js';
6
6
  export * from './tx_validator/data_validator.js';
7
+ export * from './tx_validator/double_spend_validator.js';
8
+ export * from './tx_validator/metadata_validator.js';
7
9
 
8
10
  // Used by the node to simulate public parts of transactions. Should these be moved to a shared library?
9
11
  export * from './global_variable_builder/index.js';
@@ -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,86 @@ 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
+ /**
187
+ * @notice Will call `validateHeader` to make sure that it is possible to propose
188
+ *
189
+ * @dev Throws if unable to propose
190
+ *
191
+ * @param header - The header to propose
192
+ * @param digest - The digest that attestations are signing over
193
+ *
194
+ */
195
+ public async validateBlockForSubmission(
196
+ header: Header,
197
+ attestationData: { digest: Buffer; signatures: Signature[] } = {
198
+ digest: Buffer.alloc(32),
199
+ signatures: [],
200
+ },
201
+ ): Promise<void> {
202
+ const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
203
+
204
+ const formattedSignatures = attestationData.signatures.map(attest => attest.toViemSignature());
205
+ const flags = { ignoreDA: true, ignoreSignatures: formattedSignatures.length == 0 };
206
+
207
+ const args = [
208
+ `0x${header.toBuffer().toString('hex')}`,
209
+ formattedSignatures,
210
+ `0x${attestationData.digest.toString('hex')}`,
211
+ ts,
212
+ flags,
213
+ ] as const;
214
+
161
215
  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;
216
+ await this.rollupContract.read.validateHeader(args, { account: this.account });
217
+ } catch (error: unknown) {
218
+ // Specify the type of error
219
+ if (error instanceof ContractFunctionRevertedError) {
220
+ const err = error as ContractFunctionRevertedError;
221
+ this.log.debug(`Validation failed: ${err.message}`, err.data);
222
+ } else {
223
+ this.log.debug(`Unexpected error during validation: ${error}`);
224
+ }
225
+ throw error;
168
226
  }
169
227
  }
170
228
 
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);
229
+ // @note Assumes that all ethereum slots have blocks
230
+ // Using next Ethereum block so we do NOT need to wait for it being mined before seeing the effect
231
+ public async getMetadataForSlotAtNextEthBlock(): Promise<MetadataForSlot> {
232
+ const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
233
+
234
+ const [submitter, slot, pendingBlockCount, archive] = await Promise.all([
235
+ this.rollupContract.read.getProposerAt([ts]),
236
+ this.rollupContract.read.getSlotAt([ts]),
237
+ this.rollupContract.read.pendingBlockCount(),
238
+ this.rollupContract.read.archive(),
239
+ ]);
240
+
241
+ return {
242
+ proposer: EthAddress.fromString(submitter),
243
+ slot,
244
+ pendingBlockNumber: pendingBlockCount - 1n,
245
+ archive: Buffer.from(archive.replace('0x', ''), 'hex'),
246
+ };
175
247
  }
176
248
 
177
249
  public async getCurrentEpochCommittee(): Promise<EthAddress[]> {
@@ -203,44 +275,52 @@ export class L1Publisher {
203
275
  * @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise.
204
276
  */
205
277
  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);
211
- return false;
212
- }
213
- const encodedBody = block.body.toBuffer();
278
+ const ctx = {
279
+ blockNumber: block.number,
280
+ slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
281
+ blockHash: block.hash().toString(),
282
+ };
214
283
 
215
284
  const processTxArgs = {
216
285
  header: block.header.toBuffer(),
217
286
  archive: block.archive.root.toBuffer(),
218
287
  blockHash: block.header.hash().toBuffer(),
219
- body: encodedBody,
288
+ body: block.body.toBuffer(),
220
289
  attestations,
221
290
  };
222
291
 
223
- // Process block and publish the body if needed (if not already published)
224
- while (!this.interrupted) {
292
+ // Publish body and propose block (if not already published)
293
+ if (!this.interrupted) {
225
294
  let txHash;
226
295
  const timer = new Timer();
227
296
 
228
- if (await this.checkIfTxsAreAvailable(block)) {
297
+ const isAvailable = await this.checkIfTxsAreAvailable(block);
298
+
299
+ // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
300
+ // This means that we can avoid the simulation issues in later checks.
301
+ // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
302
+ // make time consistency checks break.
303
+ await this.validateBlockForSubmission(block.header, {
304
+ digest: block.archive.root.toBuffer(),
305
+ signatures: attestations ?? [],
306
+ });
307
+
308
+ if (isAvailable) {
229
309
  this.log.verbose(`Transaction effects of block ${block.number} already published.`, ctx);
230
- txHash = await this.sendProcessTx(processTxArgs);
310
+ txHash = await this.sendProposeWithoutBodyTx(processTxArgs);
231
311
  } else {
232
- txHash = await this.sendPublishAndProcessTx(processTxArgs);
312
+ txHash = await this.sendProposeTx(processTxArgs);
233
313
  }
234
314
 
235
315
  if (!txHash) {
236
316
  this.log.info(`Failed to publish block ${block.number} to L1`, ctx);
237
- break;
317
+ return false;
238
318
  }
239
319
 
240
320
  const receipt = await this.getTransactionReceipt(txHash);
241
321
  if (!receipt) {
242
322
  this.log.info(`Failed to get receipt for tx ${txHash}`, ctx);
243
- break;
323
+ return false;
244
324
  }
245
325
 
246
326
  // Tx was mined successfully
@@ -254,17 +334,12 @@ export class L1Publisher {
254
334
  };
255
335
  this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx });
256
336
  this.metrics.recordProcessBlockTx(timer.ms(), stats);
337
+
257
338
  return true;
258
339
  }
259
340
 
260
341
  this.metrics.recordFailedTx('process');
261
342
 
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
343
  this.log.error(`Rollup.process tx status failed: ${receipt.transactionHash}`, ctx);
269
344
  await this.sleepOrInterrupted();
270
345
  }
@@ -280,7 +355,7 @@ export class L1Publisher {
280
355
  aggregationObject: Fr[],
281
356
  proof: Proof,
282
357
  ): Promise<boolean> {
283
- const ctx = { blockNumber: header.globalVariables.blockNumber };
358
+ const ctx = { blockNumber: header.globalVariables.blockNumber, slotNumber: header.globalVariables.slotNumber };
284
359
 
285
360
  const txArgs: L1SubmitProofArgs = {
286
361
  header: header.toBuffer(),
@@ -291,16 +366,16 @@ export class L1Publisher {
291
366
  };
292
367
 
293
368
  // Process block
294
- while (!this.interrupted) {
369
+ if (!this.interrupted) {
295
370
  const timer = new Timer();
296
371
  const txHash = await this.sendSubmitProofTx(txArgs);
297
372
  if (!txHash) {
298
- break;
373
+ return false;
299
374
  }
300
375
 
301
376
  const receipt = await this.getTransactionReceipt(txHash);
302
377
  if (!receipt) {
303
- break;
378
+ return false;
304
379
  }
305
380
 
306
381
  // Tx was mined successfully
@@ -341,26 +416,6 @@ export class L1Publisher {
341
416
  this.interrupted = false;
342
417
  }
343
418
 
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
419
  private async sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise<string | undefined> {
365
420
  try {
366
421
  const size = Object.values(submitProofArgs).reduce((acc, arg) => acc + arg.length, 0);
@@ -375,6 +430,12 @@ export class L1Publisher {
375
430
  `0x${proof.toString('hex')}`,
376
431
  ] as const;
377
432
 
433
+ if (!L1Publisher.SKIP_SIMULATION) {
434
+ await this.rollupContract.simulate.submitBlockRootProof(args, {
435
+ account: this.account,
436
+ });
437
+ }
438
+
378
439
  return await this.rollupContract.write.submitBlockRootProof(args, {
379
440
  account: this.account,
380
441
  });
@@ -384,12 +445,17 @@ export class L1Publisher {
384
445
  }
385
446
  }
386
447
 
448
+ // This is used in `integration_l1_publisher.test.ts` currently. Could be removed though.
387
449
  private async sendPublishTx(encodedBody: Buffer): Promise<string | undefined> {
388
- while (!this.interrupted) {
450
+ if (!this.interrupted) {
389
451
  try {
390
452
  this.log.info(`TxEffects size=${encodedBody.length} bytes`);
391
453
  const args = [`0x${encodedBody.toString('hex')}`] as const;
392
454
 
455
+ await this.availabilityOracleContract.simulate.publish(args, {
456
+ account: this.account,
457
+ });
458
+
393
459
  return await this.availabilityOracleContract.write.publish(args, {
394
460
  account: this.account,
395
461
  });
@@ -400,8 +466,8 @@ export class L1Publisher {
400
466
  }
401
467
  }
402
468
 
403
- private async sendProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
404
- while (!this.interrupted) {
469
+ private async sendProposeWithoutBodyTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
470
+ if (!this.interrupted) {
405
471
  try {
406
472
  if (encodedData.attestations) {
407
473
  const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
@@ -412,7 +478,11 @@ export class L1Publisher {
412
478
  attestations,
413
479
  ] as const;
414
480
 
415
- return await this.rollupContract.write.process(args, {
481
+ if (!L1Publisher.SKIP_SIMULATION) {
482
+ await this.rollupContract.simulate.propose(args, { account: this.account });
483
+ }
484
+
485
+ return await this.rollupContract.write.propose(args, {
416
486
  account: this.account,
417
487
  });
418
488
  } else {
@@ -422,7 +492,10 @@ export class L1Publisher {
422
492
  `0x${encodedData.blockHash.toString('hex')}`,
423
493
  ] as const;
424
494
 
425
- return await this.rollupContract.write.process(args, {
495
+ if (!L1Publisher.SKIP_SIMULATION) {
496
+ await this.rollupContract.simulate.propose(args, { account: this.account });
497
+ }
498
+ return await this.rollupContract.write.propose(args, {
426
499
  account: this.account,
427
500
  });
428
501
  }
@@ -433,10 +506,9 @@ export class L1Publisher {
433
506
  }
434
507
  }
435
508
 
436
- private async sendPublishAndProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
437
- while (!this.interrupted) {
509
+ private async sendProposeTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
510
+ if (!this.interrupted) {
438
511
  try {
439
- // @note This is quite a sin, but I'm committing war crimes in this code already.
440
512
  if (encodedData.attestations) {
441
513
  const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
442
514
  const args = [
@@ -447,7 +519,13 @@ export class L1Publisher {
447
519
  `0x${encodedData.body.toString('hex')}`,
448
520
  ] as const;
449
521
 
450
- return await this.rollupContract.write.publishAndProcess(args, {
522
+ if (!L1Publisher.SKIP_SIMULATION) {
523
+ await this.rollupContract.simulate.propose(args, {
524
+ account: this.account,
525
+ });
526
+ }
527
+
528
+ return await this.rollupContract.write.propose(args, {
451
529
  account: this.account,
452
530
  });
453
531
  } else {
@@ -458,7 +536,13 @@ export class L1Publisher {
458
536
  `0x${encodedData.body.toString('hex')}`,
459
537
  ] as const;
460
538
 
461
- return await this.rollupContract.write.publishAndProcess(args, {
539
+ if (!L1Publisher.SKIP_SIMULATION) {
540
+ await this.rollupContract.simulate.propose(args, {
541
+ account: this.account,
542
+ });
543
+ }
544
+
545
+ return await this.rollupContract.write.propose(args, {
462
546
  account: this.account,
463
547
  });
464
548
  }