@aztec/sequencer-client 0.50.1 → 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.
@@ -10,7 +10,16 @@ import {
10
10
  } from '@aztec/circuit-types';
11
11
  import { type AllowedElement, BlockProofError, PROVING_STATUS } from '@aztec/circuit-types/interfaces';
12
12
  import { type L2BlockBuiltStats } from '@aztec/circuit-types/stats';
13
- import { AztecAddress, EthAddress, type GlobalVariables, type Header, IS_DEV_NET } from '@aztec/circuits.js';
13
+ import {
14
+ AppendOnlyTreeSnapshot,
15
+ AztecAddress,
16
+ ContentCommitment,
17
+ EthAddress,
18
+ GENESIS_ARCHIVE_ROOT,
19
+ Header,
20
+ IS_DEV_NET,
21
+ StateReference,
22
+ } from '@aztec/circuits.js';
14
23
  import { Fr } from '@aztec/foundation/fields';
15
24
  import { createDebugLogger } from '@aztec/foundation/log';
16
25
  import { RunningPromise } from '@aztec/foundation/running-promise';
@@ -21,6 +30,8 @@ import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec
21
30
  import { type ValidatorClient } from '@aztec/validator-client';
22
31
  import { type WorldStateStatus, type WorldStateSynchronizer } from '@aztec/world-state';
23
32
 
33
+ import { BaseError, ContractFunctionRevertedError } from 'viem';
34
+
24
35
  import { type BlockBuilderFactory } from '../block_builder/index.js';
25
36
  import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
26
37
  import { type L1Publisher } from '../publisher/l1-publisher.js';
@@ -28,6 +39,12 @@ import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js
28
39
  import { type SequencerConfig } from './config.js';
29
40
  import { SequencerMetrics } from './metrics.js';
30
41
 
42
+ export type ShouldProposeArgs = {
43
+ pendingTxsCount?: number;
44
+ validTxsCount?: number;
45
+ processedTxsCount?: number;
46
+ };
47
+
31
48
  /**
32
49
  * Sequencer client
33
50
  * - Wins a period of time to become the sequencer (depending on finalized protocol).
@@ -183,39 +200,27 @@ export class Sequencer {
183
200
  this.state = SequencerState.IDLE;
184
201
  }
185
202
 
186
- const historicalHeader = (await this.l2BlockSource.getBlock(-1))?.header;
203
+ const chainTip = await this.l2BlockSource.getBlock(-1);
204
+ const historicalHeader = chainTip?.header;
205
+
187
206
  const newBlockNumber =
188
207
  (historicalHeader === undefined
189
208
  ? await this.l2BlockSource.getBlockNumber()
190
209
  : Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1;
191
210
 
192
- // Do not go forward with new block if not my turn
193
- if (!(await this.publisher.isItMyTurnToSubmit())) {
194
- this.log.debug('Not my turn to submit block');
195
- return;
196
- }
211
+ // If we cannot find a tip archive, assume genesis.
212
+ const chainTipArchive =
213
+ chainTip == undefined ? new Fr(GENESIS_ARCHIVE_ROOT).toBuffer() : chainTip?.archive.root.toBuffer();
197
214
 
198
- if (this.isFlushing) {
199
- this.log.verbose(`Flushing all pending txs in new block`);
215
+ let slot: bigint;
216
+ try {
217
+ slot = await this.mayProposeBlock(chainTipArchive, BigInt(newBlockNumber));
218
+ } catch (err) {
219
+ this.log.debug(`Cannot propose for block ${newBlockNumber}`);
220
+ return;
200
221
  }
201
222
 
202
- // Compute time elapsed since the previous block
203
- const lastBlockTime = historicalHeader?.globalVariables.timestamp.toNumber() || 0;
204
- const currentTime = Math.floor(Date.now() / 1000);
205
- const elapsedSinceLastBlock = currentTime - lastBlockTime;
206
- this.log.debug(
207
- `Last block mined at ${lastBlockTime} current time is ${currentTime} (elapsed ${elapsedSinceLastBlock})`,
208
- );
209
-
210
- // Do not go forward with new block if not enough time has passed since last block
211
- if (
212
- !this.isFlushing &&
213
- this.minSecondsBetweenBlocks > 0 &&
214
- elapsedSinceLastBlock < this.minSecondsBetweenBlocks
215
- ) {
216
- this.log.debug(
217
- `Not creating block because not enough time ${this.minSecondsBetweenBlocks} has passed since last block`,
218
- );
223
+ if (!this.shouldProposeBlock(historicalHeader, {})) {
219
224
  return;
220
225
  }
221
226
 
@@ -224,18 +229,8 @@ export class Sequencer {
224
229
  // Get txs to build the new block.
225
230
  const pendingTxs = this.p2pClient.getTxs('pending');
226
231
 
227
- // If we haven't hit the maxSecondsBetweenBlocks, we need to have at least minTxsPerBLock txs.
228
- if (!this.isFlushing && pendingTxs.length < this.minTxsPerBLock) {
229
- if (this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock)) {
230
- this.log.debug(
231
- `Creating block with only ${pendingTxs.length} txs as more than ${this.maxSecondsBetweenBlocks}s have passed since last block`,
232
- );
233
- } else {
234
- this.log.debug(
235
- `Not creating block because not enough txs in the pool (got ${pendingTxs.length} min ${this.minTxsPerBLock})`,
236
- );
237
- return;
238
- }
232
+ if (!this.shouldProposeBlock(historicalHeader, { pendingTxsCount: pendingTxs.length })) {
233
+ return;
239
234
  }
240
235
  this.log.debug(`Retrieved ${pendingTxs.length} txs from P2P pool`);
241
236
 
@@ -243,9 +238,17 @@ export class Sequencer {
243
238
  new Fr(newBlockNumber),
244
239
  this._coinbase,
245
240
  this._feeRecipient,
241
+ slot,
246
242
  );
247
243
 
248
- // @todo @LHerskind Include some logic to consider slots
244
+ // If I created a "partial" header here that should make our job much easier.
245
+ const proposalHeader = new Header(
246
+ new AppendOnlyTreeSnapshot(Fr.fromBuffer(chainTipArchive), 1),
247
+ ContentCommitment.empty(),
248
+ StateReference.empty(),
249
+ newGlobalVariables,
250
+ Fr.ZERO,
251
+ );
249
252
 
250
253
  // TODO: It should be responsibility of the P2P layer to validate txs before passing them on here
251
254
  const allValidTxs = await this.takeValidTxs(
@@ -261,18 +264,11 @@ export class Sequencer {
261
264
  const validTxs = this.takeTxsWithinMaxSize(allValidTxs);
262
265
 
263
266
  // Bail if we don't have enough valid txs
264
- if (
265
- !this.isFlushing &&
266
- !this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) &&
267
- validTxs.length < this.minTxsPerBLock
268
- ) {
269
- this.log.debug(
270
- `Not creating block because not enough valid txs loaded from the pool (got ${validTxs.length} min ${this.minTxsPerBLock})`,
271
- );
267
+ if (!this.shouldProposeBlock(historicalHeader, { validTxsCount: validTxs.length })) {
272
268
  return;
273
269
  }
274
270
 
275
- await this.buildBlockAndPublish(validTxs, newGlobalVariables, historicalHeader, elapsedSinceLastBlock);
271
+ await this.buildBlockAndPublish(validTxs, proposalHeader, historicalHeader);
276
272
  } catch (err) {
277
273
  if (BlockProofError.isBlockProofError(err)) {
278
274
  const txHashes = err.txHashes.filter(h => !h.isZero());
@@ -285,38 +281,125 @@ export class Sequencer {
285
281
  }
286
282
 
287
283
  /** Whether to skip the check of min txs per block if more than maxSecondsBetweenBlocks has passed since the previous block. */
288
- private skipMinTxsPerBlockCheck(elapsed: number): boolean {
284
+ private skipMinTxsPerBlockCheck(historicalHeader: Header | undefined): boolean {
285
+ const lastBlockTime = historicalHeader?.globalVariables.timestamp.toNumber() || 0;
286
+ const currentTime = Math.floor(Date.now() / 1000);
287
+ const elapsed = currentTime - lastBlockTime;
288
+
289
289
  return this.maxSecondsBetweenBlocks > 0 && elapsed >= this.maxSecondsBetweenBlocks;
290
290
  }
291
291
 
292
- @trackSpan('Sequencer.buildBlockAndPublish', (_validTxs, newGlobalVariables, _historicalHeader) => ({
293
- [Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber.toNumber(),
292
+ async mayProposeBlock(tipArchive: Buffer, proposalBlockNumber: bigint): Promise<bigint> {
293
+ // This checks that we can propose, and gives us the slot that we are to propose for
294
+ try {
295
+ const [slot, blockNumber] = await this.publisher.canProposeAtNextEthBlock(tipArchive);
296
+
297
+ if (proposalBlockNumber !== blockNumber) {
298
+ const msg = `Block number mismatch. Expected ${proposalBlockNumber} but got ${blockNumber}`;
299
+ this.log.debug(msg);
300
+ throw new Error(msg);
301
+ }
302
+
303
+ return slot;
304
+ } catch (err) {
305
+ if (err instanceof BaseError) {
306
+ const revertError = err.walk(err => err instanceof ContractFunctionRevertedError);
307
+ if (revertError instanceof ContractFunctionRevertedError) {
308
+ const errorName = revertError.data?.errorName ?? '';
309
+ this.log.debug(`canProposeAtTime failed with "${errorName}"`);
310
+ }
311
+ }
312
+ throw err;
313
+ }
314
+ }
315
+
316
+ shouldProposeBlock(historicalHeader: Header | undefined, args: ShouldProposeArgs): boolean {
317
+ if (this.isFlushing) {
318
+ this.log.verbose(`Flushing all pending txs in new block`);
319
+ return true;
320
+ }
321
+
322
+ if (IS_DEV_NET) {
323
+ // Compute time elapsed since the previous block
324
+ const lastBlockTime = historicalHeader?.globalVariables.timestamp.toNumber() || 0;
325
+ const currentTime = Math.floor(Date.now() / 1000);
326
+ const elapsedSinceLastBlock = currentTime - lastBlockTime;
327
+ this.log.debug(
328
+ `Last block mined at ${lastBlockTime} current time is ${currentTime} (elapsed ${elapsedSinceLastBlock})`,
329
+ );
330
+
331
+ // If we haven't hit the maxSecondsBetweenBlocks, we need to have at least minTxsPerBLock txs.
332
+ // Do not go forward with new block if not enough time has passed since last block
333
+ if (this.minSecondsBetweenBlocks > 0 && elapsedSinceLastBlock < this.minSecondsBetweenBlocks) {
334
+ this.log.debug(
335
+ `Not creating block because not enough time ${this.minSecondsBetweenBlocks} has passed since last block`,
336
+ );
337
+ return false;
338
+ }
339
+ }
340
+
341
+ const skipCheck = this.skipMinTxsPerBlockCheck(historicalHeader);
342
+
343
+ // If we haven't hit the maxSecondsBetweenBlocks, we need to have at least minTxsPerBLock txs.
344
+ if (args.pendingTxsCount != undefined) {
345
+ if (args.pendingTxsCount < this.minTxsPerBLock) {
346
+ if (skipCheck) {
347
+ this.log.debug(
348
+ `Creating block with only ${args.pendingTxsCount} txs as more than ${this.maxSecondsBetweenBlocks}s have passed since last block`,
349
+ );
350
+ } else {
351
+ this.log.debug(
352
+ `Not creating block because not enough txs in the pool (got ${args.pendingTxsCount} min ${this.minTxsPerBLock})`,
353
+ );
354
+ return false;
355
+ }
356
+ }
357
+ }
358
+
359
+ // Bail if we don't have enough valid txs
360
+ if (args.validTxsCount != undefined) {
361
+ // Bail if we don't have enough valid txs
362
+ if (!skipCheck && args.validTxsCount < this.minTxsPerBLock) {
363
+ this.log.debug(
364
+ `Not creating block because not enough valid txs loaded from the pool (got ${args.validTxsCount} min ${this.minTxsPerBLock})`,
365
+ );
366
+ return false;
367
+ }
368
+ }
369
+
370
+ // TODO: This check should be processedTxs.length < this.minTxsPerBLock, so we don't publish a block with
371
+ // less txs than the minimum. But that'd cause the entire block to be aborted and retried. Instead, we should
372
+ // go back to the p2p pool and load more txs until we hit our minTxsPerBLock target. Only if there are no txs
373
+ // we should bail.
374
+ if (args.processedTxsCount != undefined) {
375
+ if (args.processedTxsCount === 0 && !skipCheck && this.minTxsPerBLock > 0) {
376
+ this.log.verbose('No txs processed correctly to build block. Exiting');
377
+ return false;
378
+ }
379
+ }
380
+
381
+ return true;
382
+ }
383
+
384
+ @trackSpan('Sequencer.buildBlockAndPublish', (_validTxs, proposalHeader, _historicalHeader) => ({
385
+ [Attributes.BLOCK_NUMBER]: proposalHeader.globalVariables.blockNumber.toNumber(),
294
386
  }))
295
387
  private async buildBlockAndPublish(
296
388
  validTxs: Tx[],
297
- newGlobalVariables: GlobalVariables,
389
+ proposalHeader: Header,
298
390
  historicalHeader: Header | undefined,
299
- elapsedSinceLastBlock: number,
300
391
  ): Promise<void> {
392
+ if (!(await this.publisher.validateBlockForSubmission(proposalHeader))) {
393
+ return;
394
+ }
395
+
396
+ const newGlobalVariables = proposalHeader.globalVariables;
397
+
301
398
  this.metrics.recordNewBlock(newGlobalVariables.blockNumber.toNumber(), validTxs.length);
302
399
  const workTimer = new Timer();
303
400
  this.state = SequencerState.CREATING_BLOCK;
304
401
  this.log.info(`Building block ${newGlobalVariables.blockNumber.toNumber()} with ${validTxs.length} transactions`);
305
402
 
306
- const assertBlockHeight = async () => {
307
- const currentBlockNumber = await this.l2BlockSource.getBlockNumber();
308
- if (currentBlockNumber + 1 !== newGlobalVariables.blockNumber.toNumber()) {
309
- this.metrics.recordCancelledBlock();
310
- throw new Error('New block was emitted while building block');
311
- }
312
-
313
- if (!(await this.publisher.isItMyTurnToSubmit())) {
314
- throw new Error(`Not this sequencer turn to submit block`);
315
- }
316
-
317
- // @todo @LHerskind Should take into account, block number, proposer and slot number
318
- };
319
-
320
403
  // Get l1 to l2 messages from the contract
321
404
  this.log.debug('Requesting L1 to L2 messages from contract');
322
405
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(newGlobalVariables.blockNumber.toBigInt());
@@ -343,27 +426,21 @@ export class Sequencer {
343
426
  await this.p2pClient.deleteTxs(Tx.getHashes(failedTxData));
344
427
  }
345
428
 
346
- // TODO: This check should be processedTxs.length < this.minTxsPerBLock, so we don't publish a block with
347
- // less txs than the minimum. But that'd cause the entire block to be aborted and retried. Instead, we should
348
- // go back to the p2p pool and load more txs until we hit our minTxsPerBLock target. Only if there are no txs
349
- // we should bail.
350
429
  if (
351
- !this.isFlushing &&
352
- processedTxs.length === 0 &&
353
- !this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) &&
354
- this.minTxsPerBLock > 0
430
+ !(await this.publisher.validateBlockForSubmission(proposalHeader)) ||
431
+ !this.shouldProposeBlock(historicalHeader, {
432
+ validTxsCount: validTxs.length,
433
+ processedTxsCount: processedTxs.length,
434
+ })
355
435
  ) {
356
- this.log.verbose('No txs processed correctly to build block. Exiting');
357
436
  blockBuilder.cancelBlock();
358
437
  return;
359
438
  }
360
439
 
361
- await assertBlockHeight();
362
-
363
440
  // All real transactions have been added, set the block as full and complete the proving.
364
441
  await blockBuilder.setBlockCompleted();
365
442
 
366
- // Here we are now waiting for the block to be proven.
443
+ // Here we are now waiting for the block to be proven (using simulated[fake] proofs).
367
444
  // TODO(@PhilWindle) We should probably periodically check for things like another
368
445
  // block being published before ours instead of just waiting on our block
369
446
  const result = await blockTicket.provingPromise;
@@ -371,12 +448,12 @@ export class Sequencer {
371
448
  throw new Error(`Block proving failed, reason: ${result.reason}`);
372
449
  }
373
450
 
374
- await assertBlockHeight();
375
-
376
- // Block is ready, now finalise and publish!
451
+ // Block is ready, now finalise
377
452
  const { block } = await blockBuilder.finaliseBlock();
378
453
 
379
- await assertBlockHeight();
454
+ if (!(await this.publisher.validateBlockForSubmission(block.header))) {
455
+ return;
456
+ }
380
457
 
381
458
  const workDuration = workTimer.ms();
382
459
  this.log.verbose(
@@ -395,10 +472,11 @@ export class Sequencer {
395
472
  if (this.isFlushing) {
396
473
  this.log.verbose(`Flushing completed`);
397
474
  }
475
+
398
476
  this.isFlushing = false;
477
+ const attestations = await this.collectAttestations(block);
399
478
 
400
479
  try {
401
- const attestations = await this.collectAttestations(block);
402
480
  await this.publishL2Block(block, attestations);
403
481
  this.metrics.recordPublishedBlock(workDuration);
404
482
  this.log.info(
@@ -431,12 +509,18 @@ export class Sequencer {
431
509
  // ; ;
432
510
  // / \
433
511
  // _____________/_ __ \_____________
512
+
434
513
  if (IS_DEV_NET || !this.validatorClient) {
435
514
  return undefined;
436
515
  }
437
516
 
438
517
  // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached
439
518
  const committee = await this.publisher.getCurrentEpochCommittee();
519
+
520
+ if (committee.length === 0) {
521
+ return undefined;
522
+ }
523
+
440
524
  const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
441
525
 
442
526
  // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7974): we do not have transaction[] lists in the block for now
@@ -448,10 +532,7 @@ export class Sequencer {
448
532
  this.validatorClient.broadcastBlockProposal(proposal);
449
533
 
450
534
  this.state = SequencerState.WAITING_FOR_ATTESTATIONS;
451
- const attestations = await this.validatorClient.collectAttestations(
452
- proposal.header.globalVariables.slotNumber.toBigInt(),
453
- numberOfRequiredAttestations,
454
- );
535
+ const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations);
455
536
 
456
537
  // note: the smart contract requires that the signatures are provided in the order of the committee
457
538
  return await orderAttestations(attestations, committee);