@aztec/sequencer-client 0.49.2 → 0.51.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 (37) hide show
  1. package/dest/client/sequencer-client.d.ts +6 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +13 -7
  4. package/dest/global_variable_builder/global_builder.d.ts +6 -54
  5. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  6. package/dest/global_variable_builder/global_builder.js +24 -12
  7. package/dest/global_variable_builder/index.d.ts +0 -9
  8. package/dest/global_variable_builder/index.d.ts.map +1 -1
  9. package/dest/global_variable_builder/index.js +2 -12
  10. package/dest/publisher/index.d.ts +0 -8
  11. package/dest/publisher/index.d.ts.map +1 -1
  12. package/dest/publisher/index.js +1 -10
  13. package/dest/publisher/l1-publisher.d.ts +26 -70
  14. package/dest/publisher/l1-publisher.d.ts.map +1 -1
  15. package/dest/publisher/l1-publisher.js +168 -19
  16. package/dest/sequencer/sequencer.d.ts +22 -8
  17. package/dest/sequencer/sequencer.d.ts.map +1 -1
  18. package/dest/sequencer/sequencer.js +76 -14
  19. package/package.json +18 -17
  20. package/src/client/sequencer-client.ts +13 -4
  21. package/src/global_variable_builder/global_builder.ts +38 -62
  22. package/src/global_variable_builder/index.ts +0 -15
  23. package/src/publisher/index.ts +0 -14
  24. package/src/publisher/l1-publisher.ts +217 -95
  25. package/src/sequencer/sequencer.ts +97 -11
  26. package/dest/global_variable_builder/viem-reader.d.ts +0 -17
  27. package/dest/global_variable_builder/viem-reader.d.ts.map +0 -1
  28. package/dest/global_variable_builder/viem-reader.js +0 -40
  29. package/dest/publisher/viem-tx-sender.d.ts +0 -59
  30. package/dest/publisher/viem-tx-sender.d.ts.map +0 -1
  31. package/dest/publisher/viem-tx-sender.js +0 -236
  32. package/dest/receiver.d.ts +0 -10
  33. package/dest/receiver.d.ts.map +0 -1
  34. package/dest/receiver.js +0 -2
  35. package/src/global_variable_builder/viem-reader.ts +0 -64
  36. package/src/publisher/viem-tx-sender.ts +0 -296
  37. package/src/receiver.ts +0 -11
@@ -1,17 +1,34 @@
1
- import { type L2Block } from '@aztec/circuit-types';
1
+ import { type L2Block, type Signature } from '@aztec/circuit-types';
2
2
  import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats';
3
- import { type EthAddress, type Header, type Proof } from '@aztec/circuits.js';
3
+ import { ETHEREUM_SLOT_DURATION, EthAddress, type Header, type Proof } from '@aztec/circuits.js';
4
+ import { createEthereumChain } from '@aztec/ethereum';
4
5
  import { type Fr } from '@aztec/foundation/fields';
5
6
  import { createDebugLogger } from '@aztec/foundation/log';
6
7
  import { serializeToBuffer } from '@aztec/foundation/serialize';
7
8
  import { InterruptibleSleep } from '@aztec/foundation/sleep';
8
9
  import { Timer } from '@aztec/foundation/timer';
10
+ import { AvailabilityOracleAbi, RollupAbi } from '@aztec/l1-artifacts';
9
11
  import { type TelemetryClient } from '@aztec/telemetry-client';
10
12
 
11
13
  import pick from 'lodash.pick';
12
-
13
- import { type L2BlockReceiver } from '../receiver.js';
14
- import { type PublisherConfig } from './config.js';
14
+ import {
15
+ type GetContractReturnType,
16
+ type Hex,
17
+ type HttpTransport,
18
+ type PrivateKeyAccount,
19
+ type PublicClient,
20
+ type WalletClient,
21
+ createPublicClient,
22
+ createWalletClient,
23
+ getAddress,
24
+ getContract,
25
+ hexToBytes,
26
+ http,
27
+ } from 'viem';
28
+ import { privateKeyToAccount } from 'viem/accounts';
29
+ import type * as chains from 'viem/chains';
30
+
31
+ import { type PublisherConfig, type TxSenderConfig } from './config.js';
15
32
  import { L1PublisherMetrics } from './l1-publisher-metrics.js';
16
33
 
17
34
  /**
@@ -27,7 +44,7 @@ export type TransactionStats = {
27
44
  };
28
45
 
29
46
  /**
30
- * Minimal information from a tx receipt returned by an L1PublisherTxSender.
47
+ * Minimal information from a tx receipt.
31
48
  */
32
49
  export type MinimalTransactionReceipt = {
33
50
  /** True if the tx was successful, false if reverted. */
@@ -49,84 +66,18 @@ export type MinimalTransactionReceipt = {
49
66
  */
50
67
  export type Attestation = { isEmpty: boolean; v: number; r: `0x${string}`; s: `0x${string}` };
51
68
 
52
- /**
53
- * Pushes txs to the L1 chain and waits for their completion.
54
- */
55
- export interface L1PublisherTxSender {
56
- /** Attests to the given archive root. */
57
- attest(archive: `0x${string}`): Promise<Attestation>;
58
-
59
- /** Returns the EOA used for sending txs to L1. */
60
- getSenderAddress(): Promise<EthAddress>;
61
-
62
- /** Returns the address of the L2 proposer at the NEXT Ethereum block zero if anyone can submit. */
63
- getProposerAtNextEthBlock(): Promise<EthAddress>;
64
-
65
- /**
66
- * Publishes tx effects to Availability Oracle.
67
- * @param encodedBody - Encoded block body.
68
- * @returns The hash of the mined tx.
69
- */
70
- sendPublishTx(encodedBody: Buffer): Promise<string | undefined>;
71
-
72
- /**
73
- * Sends a tx to the L1 rollup contract with a new L2 block. Returns once the tx has been mined.
74
- * @param encodedData - Serialized data for processing the new L2 block.
75
- * @returns The hash of the mined tx.
76
- */
77
- sendProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined>;
78
-
79
- /**
80
- * Publishes tx effects to availability oracle and send L2 block to rollup contract
81
- * @param encodedData - Data for processing the new L2 block.
82
- * @returns The hash of the tx.
83
- */
84
- sendPublishAndProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined>;
85
-
86
- /**
87
- * Sends a tx to the L1 rollup contract with a proof. Returns once the tx has been mined.
88
- * @param encodedData - Serialized data for processing the new L2 block.
89
- * @returns The hash of the mined tx.
90
- */
91
- sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise<string | undefined>;
92
-
93
- /**
94
- * Returns a tx receipt if the tx has been mined.
95
- * @param txHash - Hash of the tx to look for.
96
- * @returns Undefined if the tx hasn't been mined yet, the receipt otherwise.
97
- */
98
- getTransactionReceipt(txHash: string): Promise<MinimalTransactionReceipt | undefined>;
99
-
100
- /**
101
- * Returns info on a tx by calling eth_getTransaction.
102
- * @param txHash - Hash of the tx to look for.
103
- */
104
- getTransactionStats(txHash: string): Promise<TransactionStats | undefined>;
105
-
106
- /**
107
- * Returns the current archive root.
108
- * @returns The current archive root of the rollup contract.
109
- */
110
- getCurrentArchive(): Promise<Buffer>;
111
-
112
- /**
113
- * Checks if the transaction effects of the given block are available.
114
- * @param block - The block of which to check whether txs are available.
115
- * @returns True if the txs are available, false otherwise.
116
- */
117
- checkIfTxsAreAvailable(block: L2Block): Promise<boolean>;
118
- }
119
-
120
69
  /** Arguments to the process method of the rollup contract */
121
70
  export type L1ProcessArgs = {
122
71
  /** The L2 block header. */
123
72
  header: Buffer;
124
73
  /** A root of the archive tree after the L2 block is applied. */
125
74
  archive: Buffer;
75
+ /** The L2 block's leaf in the archive tree. */
76
+ blockHash: Buffer;
126
77
  /** L2 block body. */
127
78
  body: Buffer;
128
79
  /** Attestations */
129
- attestations?: Attestation[];
80
+ attestations?: Signature[];
130
81
  };
131
82
 
132
83
  /** Arguments to the submitProof method of the rollup contract */
@@ -151,38 +102,107 @@ export type L1SubmitProofArgs = {
151
102
  *
152
103
  * Adapted from https://github.com/AztecProtocol/aztec2-internal/blob/master/falafel/src/rollup_publisher.ts.
153
104
  */
154
- export class L1Publisher implements L2BlockReceiver {
105
+ export class L1Publisher {
155
106
  private interruptibleSleep = new InterruptibleSleep();
156
107
  private sleepTimeMs: number;
157
108
  private interrupted = false;
158
109
  private metrics: L1PublisherMetrics;
159
110
  private log = createDebugLogger('aztec:sequencer:publisher');
160
111
 
161
- constructor(private txSender: L1PublisherTxSender, client: TelemetryClient, config?: PublisherConfig) {
112
+ private availabilityOracleContract: GetContractReturnType<
113
+ typeof AvailabilityOracleAbi,
114
+ WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>
115
+ >;
116
+ private rollupContract: GetContractReturnType<
117
+ typeof RollupAbi,
118
+ WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>
119
+ >;
120
+ private publicClient: PublicClient<HttpTransport, chains.Chain>;
121
+ private account: PrivateKeyAccount;
122
+
123
+ constructor(config: TxSenderConfig & PublisherConfig, client: TelemetryClient) {
162
124
  this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000;
163
125
  this.metrics = new L1PublisherMetrics(client, 'L1Publisher');
126
+
127
+ const { l1RpcUrl: rpcUrl, l1ChainId: chainId, publisherPrivateKey, l1Contracts } = config;
128
+ const chain = createEthereumChain(rpcUrl, chainId);
129
+ this.account = privateKeyToAccount(publisherPrivateKey);
130
+ const walletClient = createWalletClient({
131
+ account: this.account,
132
+ chain: chain.chainInfo,
133
+ transport: http(chain.rpcUrl),
134
+ });
135
+
136
+ this.publicClient = createPublicClient({
137
+ chain: chain.chainInfo,
138
+ transport: http(chain.rpcUrl),
139
+ });
140
+
141
+ this.availabilityOracleContract = getContract({
142
+ address: getAddress(l1Contracts.availabilityOracleAddress.toString()),
143
+ abi: AvailabilityOracleAbi,
144
+ client: walletClient,
145
+ });
146
+ this.rollupContract = getContract({
147
+ address: getAddress(l1Contracts.rollupAddress.toString()),
148
+ abi: RollupAbi,
149
+ client: walletClient,
150
+ });
164
151
  }
165
152
 
166
- public async attest(archive: `0x${string}`): Promise<Attestation> {
167
- return await this.txSender.attest(archive);
153
+ public getSenderAddress(): Promise<EthAddress> {
154
+ return Promise.resolve(EthAddress.fromString(this.account.address));
168
155
  }
169
156
 
170
- public async senderAddress(): Promise<EthAddress> {
171
- return await this.txSender.getSenderAddress();
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> {
161
+ 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;
168
+ }
172
169
  }
173
170
 
174
171
  public async isItMyTurnToSubmit(): Promise<boolean> {
175
- const submitter = await this.txSender.getProposerAtNextEthBlock();
176
- const sender = await this.txSender.getSenderAddress();
172
+ const submitter = await this.getProposerAtNextEthBlock();
173
+ const sender = await this.getSenderAddress();
177
174
  return submitter.isZero() || submitter.equals(sender);
178
175
  }
179
176
 
177
+ public async getCurrentEpochCommittee(): Promise<EthAddress[]> {
178
+ const committee = await this.rollupContract.read.getCurrentEpochCommittee();
179
+ return committee.map(EthAddress.fromString);
180
+ }
181
+
182
+ checkIfTxsAreAvailable(block: L2Block): Promise<boolean> {
183
+ const args = [`0x${block.body.getTxsEffectsHash().toString('hex').padStart(64, '0')}`] as const;
184
+ return this.availabilityOracleContract.read.isAvailable(args);
185
+ }
186
+
187
+ async getTransactionStats(txHash: string): Promise<TransactionStats | undefined> {
188
+ const tx = await this.publicClient.getTransaction({ hash: txHash as Hex });
189
+ if (!tx) {
190
+ return undefined;
191
+ }
192
+ const calldata = hexToBytes(tx.input);
193
+ return {
194
+ transactionHash: tx.hash,
195
+ calldataSize: calldata.length,
196
+ calldataGas: getCalldataGasUsage(calldata),
197
+ };
198
+ }
199
+
180
200
  /**
181
201
  * Publishes L2 block on L1.
182
202
  * @param block - L2 block to publish.
183
203
  * @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise.
184
204
  */
185
- public async processL2Block(block: L2Block, attestations?: Attestation[]): Promise<boolean> {
205
+ public async processL2Block(block: L2Block, attestations?: Signature[]): Promise<boolean> {
186
206
  const ctx = { blockNumber: block.number, blockHash: block.hash().toString() };
187
207
  // TODO(#4148) Remove this block number check, it's here because we don't currently have proper genesis state on the contract
188
208
  const lastArchive = block.header.lastArchive.root.toBuffer();
@@ -195,6 +215,7 @@ export class L1Publisher implements L2BlockReceiver {
195
215
  const processTxArgs = {
196
216
  header: block.header.toBuffer(),
197
217
  archive: block.archive.root.toBuffer(),
218
+ blockHash: block.header.hash().toBuffer(),
198
219
  body: encodedBody,
199
220
  attestations,
200
221
  };
@@ -204,7 +225,7 @@ export class L1Publisher implements L2BlockReceiver {
204
225
  let txHash;
205
226
  const timer = new Timer();
206
227
 
207
- if (await this.txSender.checkIfTxsAreAvailable(block)) {
228
+ if (await this.checkIfTxsAreAvailable(block)) {
208
229
  this.log.verbose(`Transaction effects of block ${block.number} already published.`, ctx);
209
230
  txHash = await this.sendProcessTx(processTxArgs);
210
231
  } else {
@@ -224,7 +245,7 @@ export class L1Publisher implements L2BlockReceiver {
224
245
 
225
246
  // Tx was mined successfully
226
247
  if (receipt.status) {
227
- const tx = await this.txSender.getTransactionStats(txHash);
248
+ const tx = await this.getTransactionStats(txHash);
228
249
  const stats: L1PublishBlockStats = {
229
250
  ...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
230
251
  ...pick(tx!, 'calldataGas', 'calldataSize'),
@@ -284,13 +305,13 @@ export class L1Publisher implements L2BlockReceiver {
284
305
 
285
306
  // Tx was mined successfully
286
307
  if (receipt.status) {
287
- const tx = await this.txSender.getTransactionStats(txHash);
308
+ const tx = await this.getTransactionStats(txHash);
288
309
  const stats: L1PublishProofStats = {
289
310
  ...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
290
311
  ...pick(tx!, 'calldataGas', 'calldataSize'),
291
312
  eventName: 'proof-published-to-l1',
292
313
  };
293
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx });
314
+ this.log.info(`Published proof to L1 rollup contract`, { ...stats, ...ctx });
294
315
  this.metrics.recordSubmitProof(timer.ms(), stats);
295
316
  return true;
296
317
  }
@@ -320,13 +341,18 @@ export class L1Publisher implements L2BlockReceiver {
320
341
  this.interrupted = false;
321
342
  }
322
343
 
344
+ async getCurrentArchive(): Promise<Buffer> {
345
+ const archive = await this.rollupContract.read.archive();
346
+ return Buffer.from(archive.replace('0x', ''), 'hex');
347
+ }
348
+
323
349
  /**
324
350
  * Verifies that the given value of last archive in a block header equals current archive of the rollup contract
325
351
  * @param lastArchive - The last archive of the block we wish to publish.
326
352
  * @returns Boolean indicating if the hashes are equal.
327
353
  */
328
354
  private async checkLastArchiveHash(lastArchive: Buffer): Promise<boolean> {
329
- const fromChain = await this.txSender.getCurrentArchive();
355
+ const fromChain = await this.getCurrentArchive();
330
356
  const areSame = lastArchive.equals(fromChain);
331
357
  if (!areSame) {
332
358
  this.log.debug(`Contract archive: ${fromChain.toString('hex')}`);
@@ -339,7 +365,19 @@ export class L1Publisher implements L2BlockReceiver {
339
365
  try {
340
366
  const size = Object.values(submitProofArgs).reduce((acc, arg) => acc + arg.length, 0);
341
367
  this.log.info(`SubmitProof size=${size} bytes`);
342
- return await this.txSender.sendSubmitProofTx(submitProofArgs);
368
+
369
+ const { header, archive, proverId, aggregationObject, proof } = submitProofArgs;
370
+ const args = [
371
+ `0x${header.toString('hex')}`,
372
+ `0x${archive.toString('hex')}`,
373
+ `0x${proverId.toString('hex')}`,
374
+ `0x${aggregationObject.toString('hex')}`,
375
+ `0x${proof.toString('hex')}`,
376
+ ] as const;
377
+
378
+ return await this.rollupContract.write.submitBlockRootProof(args, {
379
+ account: this.account,
380
+ });
343
381
  } catch (err) {
344
382
  this.log.error(`Rollup submit proof failed`, err);
345
383
  return undefined;
@@ -350,7 +388,11 @@ export class L1Publisher implements L2BlockReceiver {
350
388
  while (!this.interrupted) {
351
389
  try {
352
390
  this.log.info(`TxEffects size=${encodedBody.length} bytes`);
353
- return await this.txSender.sendPublishTx(encodedBody);
391
+ const args = [`0x${encodedBody.toString('hex')}`] as const;
392
+
393
+ return await this.availabilityOracleContract.write.publish(args, {
394
+ account: this.account,
395
+ });
354
396
  } catch (err) {
355
397
  this.log.error(`TxEffects publish failed`, err);
356
398
  return undefined;
@@ -361,7 +403,29 @@ export class L1Publisher implements L2BlockReceiver {
361
403
  private async sendProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
362
404
  while (!this.interrupted) {
363
405
  try {
364
- return await this.txSender.sendProcessTx(encodedData);
406
+ if (encodedData.attestations) {
407
+ const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
408
+ const args = [
409
+ `0x${encodedData.header.toString('hex')}`,
410
+ `0x${encodedData.archive.toString('hex')}`,
411
+ `0x${encodedData.blockHash.toString('hex')}`,
412
+ attestations,
413
+ ] as const;
414
+
415
+ return await this.rollupContract.write.process(args, {
416
+ account: this.account,
417
+ });
418
+ } else {
419
+ const args = [
420
+ `0x${encodedData.header.toString('hex')}`,
421
+ `0x${encodedData.archive.toString('hex')}`,
422
+ `0x${encodedData.blockHash.toString('hex')}`,
423
+ ] as const;
424
+
425
+ return await this.rollupContract.write.process(args, {
426
+ account: this.account,
427
+ });
428
+ }
365
429
  } catch (err) {
366
430
  this.log.error(`Rollup publish failed`, err);
367
431
  return undefined;
@@ -372,7 +436,32 @@ export class L1Publisher implements L2BlockReceiver {
372
436
  private async sendPublishAndProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
373
437
  while (!this.interrupted) {
374
438
  try {
375
- return await this.txSender.sendPublishAndProcessTx(encodedData);
439
+ // @note This is quite a sin, but I'm committing war crimes in this code already.
440
+ if (encodedData.attestations) {
441
+ const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
442
+ const args = [
443
+ `0x${encodedData.header.toString('hex')}`,
444
+ `0x${encodedData.archive.toString('hex')}`,
445
+ `0x${encodedData.blockHash.toString('hex')}`,
446
+ attestations,
447
+ `0x${encodedData.body.toString('hex')}`,
448
+ ] as const;
449
+
450
+ return await this.rollupContract.write.publishAndProcess(args, {
451
+ account: this.account,
452
+ });
453
+ } else {
454
+ const args = [
455
+ `0x${encodedData.header.toString('hex')}`,
456
+ `0x${encodedData.archive.toString('hex')}`,
457
+ `0x${encodedData.blockHash.toString('hex')}`,
458
+ `0x${encodedData.body.toString('hex')}`,
459
+ ] as const;
460
+
461
+ return await this.rollupContract.write.publishAndProcess(args, {
462
+ account: this.account,
463
+ });
464
+ }
376
465
  } catch (err) {
377
466
  this.log.error(`Rollup publish failed`, err);
378
467
  return undefined;
@@ -380,10 +469,34 @@ export class L1Publisher implements L2BlockReceiver {
380
469
  }
381
470
  }
382
471
 
383
- private async getTransactionReceipt(txHash: string): Promise<MinimalTransactionReceipt | undefined> {
472
+ /**
473
+ * Returns a tx receipt if the tx has been mined.
474
+ * @param txHash - Hash of the tx to look for.
475
+ * @returns Undefined if the tx hasn't been mined yet, the receipt otherwise.
476
+ */
477
+ async getTransactionReceipt(txHash: string): Promise<MinimalTransactionReceipt | undefined> {
384
478
  while (!this.interrupted) {
385
479
  try {
386
- return await this.txSender.getTransactionReceipt(txHash);
480
+ const receipt = await this.publicClient.getTransactionReceipt({
481
+ hash: txHash as Hex,
482
+ });
483
+
484
+ if (receipt) {
485
+ if (receipt.transactionHash !== txHash) {
486
+ throw new Error(`Tx hash mismatch: ${receipt.transactionHash} !== ${txHash}`);
487
+ }
488
+
489
+ return {
490
+ status: receipt.status === 'success',
491
+ transactionHash: txHash,
492
+ gasUsed: receipt.gasUsed,
493
+ gasPrice: receipt.effectiveGasPrice,
494
+ logs: receipt.logs,
495
+ };
496
+ }
497
+
498
+ this.log.debug(`Receipt not found for tx hash ${txHash}`);
499
+ return undefined;
387
500
  } catch (err) {
388
501
  //this.log.error(`Error getting tx receipt`, err);
389
502
  await this.sleepOrInterrupted();
@@ -395,3 +508,12 @@ export class L1Publisher implements L2BlockReceiver {
395
508
  await this.interruptibleSleep.sleep(this.sleepTimeMs);
396
509
  }
397
510
  }
511
+
512
+ /**
513
+ * Returns cost of calldata usage in Ethereum.
514
+ * @param data - Calldata.
515
+ * @returns 4 for each zero byte, 16 for each nonzero.
516
+ */
517
+ function getCalldataGasUsage(data: Uint8Array) {
518
+ return data.filter(byte => byte === 0).length * 4 + data.filter(byte => byte !== 0).length * 16;
519
+ }
@@ -1,8 +1,10 @@
1
1
  import {
2
+ type BlockAttestation,
2
3
  type L1ToL2MessageSource,
3
4
  type L2Block,
4
5
  type L2BlockSource,
5
6
  type ProcessedTx,
7
+ Signature,
6
8
  Tx,
7
9
  type TxValidator,
8
10
  } from '@aztec/circuit-types';
@@ -16,11 +18,12 @@ import { Timer, elapsed } from '@aztec/foundation/timer';
16
18
  import { type P2P } from '@aztec/p2p';
17
19
  import { type PublicProcessorFactory } from '@aztec/simulator';
18
20
  import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client';
21
+ import { type ValidatorClient } from '@aztec/validator-client';
19
22
  import { type WorldStateStatus, type WorldStateSynchronizer } from '@aztec/world-state';
20
23
 
21
24
  import { type BlockBuilderFactory } from '../block_builder/index.js';
22
25
  import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
23
- import { type Attestation, type L1Publisher } from '../publisher/l1-publisher.js';
26
+ import { type L1Publisher } from '../publisher/l1-publisher.js';
24
27
  import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js';
25
28
  import { type SequencerConfig } from './config.js';
26
29
  import { SequencerMetrics } from './metrics.js';
@@ -50,9 +53,11 @@ export class Sequencer {
50
53
  private allowedInTeardown: AllowedElement[] = [];
51
54
  private maxBlockSizeInBytes: number = 1024 * 1024;
52
55
  private metrics: SequencerMetrics;
56
+ private isFlushing: boolean = false;
53
57
 
54
58
  constructor(
55
59
  private publisher: L1Publisher,
60
+ private validatorClient: ValidatorClient | undefined, // During migration the validator client can be inactive
56
61
  private globalsBuilder: GlobalVariableBuilder,
57
62
  private p2pClient: P2P,
58
63
  private worldState: WorldStateSynchronizer,
@@ -190,6 +195,10 @@ export class Sequencer {
190
195
  return;
191
196
  }
192
197
 
198
+ if (this.isFlushing) {
199
+ this.log.verbose(`Flushing all pending txs in new block`);
200
+ }
201
+
193
202
  // Compute time elapsed since the previous block
194
203
  const lastBlockTime = historicalHeader?.globalVariables.timestamp.toNumber() || 0;
195
204
  const currentTime = Math.floor(Date.now() / 1000);
@@ -199,7 +208,11 @@ export class Sequencer {
199
208
  );
200
209
 
201
210
  // Do not go forward with new block if not enough time has passed since last block
202
- if (this.minSecondsBetweenBlocks > 0 && elapsedSinceLastBlock < this.minSecondsBetweenBlocks) {
211
+ if (
212
+ !this.isFlushing &&
213
+ this.minSecondsBetweenBlocks > 0 &&
214
+ elapsedSinceLastBlock < this.minSecondsBetweenBlocks
215
+ ) {
203
216
  this.log.debug(
204
217
  `Not creating block because not enough time ${this.minSecondsBetweenBlocks} has passed since last block`,
205
218
  );
@@ -212,7 +225,7 @@ export class Sequencer {
212
225
  const pendingTxs = this.p2pClient.getTxs('pending');
213
226
 
214
227
  // If we haven't hit the maxSecondsBetweenBlocks, we need to have at least minTxsPerBLock txs.
215
- if (pendingTxs.length < this.minTxsPerBLock) {
228
+ if (!this.isFlushing && pendingTxs.length < this.minTxsPerBLock) {
216
229
  if (this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock)) {
217
230
  this.log.debug(
218
231
  `Creating block with only ${pendingTxs.length} txs as more than ${this.maxSecondsBetweenBlocks}s have passed since last block`,
@@ -248,7 +261,11 @@ export class Sequencer {
248
261
  const validTxs = this.takeTxsWithinMaxSize(allValidTxs);
249
262
 
250
263
  // Bail if we don't have enough valid txs
251
- if (!this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) && validTxs.length < this.minTxsPerBLock) {
264
+ if (
265
+ !this.isFlushing &&
266
+ !this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) &&
267
+ validTxs.length < this.minTxsPerBLock
268
+ ) {
252
269
  this.log.debug(
253
270
  `Not creating block because not enough valid txs loaded from the pool (got ${validTxs.length} min ${this.minTxsPerBLock})`,
254
271
  );
@@ -330,7 +347,12 @@ export class Sequencer {
330
347
  // less txs than the minimum. But that'd cause the entire block to be aborted and retried. Instead, we should
331
348
  // go back to the p2p pool and load more txs until we hit our minTxsPerBLock target. Only if there are no txs
332
349
  // we should bail.
333
- if (processedTxs.length === 0 && !this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) && this.minTxsPerBLock > 0) {
350
+ if (
351
+ !this.isFlushing &&
352
+ processedTxs.length === 0 &&
353
+ !this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) &&
354
+ this.minTxsPerBLock > 0
355
+ ) {
334
356
  this.log.verbose('No txs processed correctly to build block. Exiting');
335
357
  blockBuilder.cancelBlock();
336
358
  return;
@@ -370,6 +392,11 @@ export class Sequencer {
370
392
  } satisfies L2BlockBuiltStats,
371
393
  );
372
394
 
395
+ if (this.isFlushing) {
396
+ this.log.verbose(`Flushing completed`);
397
+ }
398
+ this.isFlushing = false;
399
+
373
400
  try {
374
401
  const attestations = await this.collectAttestations(block);
375
402
  await this.publishL2Block(block, attestations);
@@ -377,7 +404,7 @@ export class Sequencer {
377
404
  this.log.info(
378
405
  `Submitted rollup block ${block.number} with ${
379
406
  processedTxs.length
380
- } transactions duration=${workDuration}ms (Submitter: ${await this.publisher.senderAddress()})`,
407
+ } transactions duration=${workDuration}ms (Submitter: ${await this.publisher.getSenderAddress()})`,
381
408
  );
382
409
  } catch (err) {
383
410
  this.metrics.recordFailedBlock();
@@ -385,7 +412,12 @@ export class Sequencer {
385
412
  }
386
413
  }
387
414
 
388
- protected async collectAttestations(block: L2Block): Promise<Attestation[] | undefined> {
415
+ /** Forces the sequencer to bypass all time and tx count checks for the next block and build anyway. */
416
+ public flush() {
417
+ this.isFlushing = true;
418
+ }
419
+
420
+ protected async collectAttestations(block: L2Block): Promise<Signature[] | undefined> {
389
421
  // @todo This should collect attestations properly and fix the ordering of them to make sense
390
422
  // the current implementation is a PLACEHOLDER and should be nuked from orbit.
391
423
  // It is assuming that there will only be ONE (1) validator, so only one attestation
@@ -399,12 +431,30 @@ export class Sequencer {
399
431
  // ; ;
400
432
  // / \
401
433
  // _____________/_ __ \_____________
402
- if (IS_DEV_NET) {
434
+ if (IS_DEV_NET || !this.validatorClient) {
403
435
  return undefined;
404
436
  }
405
437
 
406
- const myAttestation = await this.publisher.attest(block.archive.root.toString());
407
- return [myAttestation];
438
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached
439
+ const committee = await this.publisher.getCurrentEpochCommittee();
440
+ const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
441
+
442
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7974): we do not have transaction[] lists in the block for now
443
+ // Dont do anything with the proposals for now - just collect them
444
+
445
+ const proposal = await this.validatorClient.createBlockProposal(block.header, block.archive.root, []);
446
+
447
+ this.state = SequencerState.PUBLISHING_BLOCK_TO_PEERS;
448
+ this.validatorClient.broadcastBlockProposal(proposal);
449
+
450
+ this.state = SequencerState.WAITING_FOR_ATTESTATIONS;
451
+ const attestations = await this.validatorClient.collectAttestations(
452
+ proposal.header.globalVariables.slotNumber.toBigInt(),
453
+ numberOfRequiredAttestations,
454
+ );
455
+
456
+ // note: the smart contract requires that the signatures are provided in the order of the committee
457
+ return await orderAttestations(attestations, committee);
408
458
  }
409
459
 
410
460
  /**
@@ -414,9 +464,10 @@ export class Sequencer {
414
464
  @trackSpan('Sequencer.publishL2Block', block => ({
415
465
  [Attributes.BLOCK_NUMBER]: block.number,
416
466
  }))
417
- protected async publishL2Block(block: L2Block, attestations?: Attestation[]) {
467
+ protected async publishL2Block(block: L2Block, attestations?: Signature[]) {
418
468
  // Publishes new block to the network and awaits the tx to be mined
419
469
  this.state = SequencerState.PUBLISHING_BLOCK;
470
+
420
471
  const publishedL2Block = await this.publisher.processL2Block(block, attestations);
421
472
  if (publishedL2Block) {
422
473
  this.lastPublishedBlock = block.number;
@@ -503,6 +554,14 @@ export enum SequencerState {
503
554
  * Creating a new L2 block. Includes processing public function calls and running rollup circuits. Will move to PUBLISHING_CONTRACT_DATA.
504
555
  */
505
556
  CREATING_BLOCK,
557
+ /**
558
+ * Publishing blocks to validator peers. Will move to WAITING_FOR_ATTESTATIONS.
559
+ */
560
+ PUBLISHING_BLOCK_TO_PEERS,
561
+ /**
562
+ * The block has been published to peers, and we are waiting for attestations. Will move to PUBLISHING_CONTRACT_DATA.
563
+ */
564
+ WAITING_FOR_ATTESTATIONS,
506
565
  /**
507
566
  * Sending the tx to L1 with encrypted logs and awaiting it to be mined. Will move back to PUBLISHING_BLOCK once finished.
508
567
  */
@@ -516,3 +575,30 @@ export enum SequencerState {
516
575
  */
517
576
  STOPPED,
518
577
  }
578
+
579
+ /** Order Attestations
580
+ *
581
+ * Returns attestation signatures in the order of a series of provided ethereum addresses
582
+ * The rollup smart contract expects attestations to appear in the order of the committee
583
+ *
584
+ * @todo: perform this logic within the memory attestation store instead?
585
+ */
586
+ async function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): Promise<Signature[]> {
587
+ // Create a map of sender addresses to BlockAttestations
588
+ const attestationMap = new Map<string, BlockAttestation>();
589
+
590
+ for (const attestation of attestations) {
591
+ const sender = await attestation.getSender();
592
+ if (sender) {
593
+ attestationMap.set(sender.toString(), attestation);
594
+ }
595
+ }
596
+
597
+ // Create the ordered array based on the orderAddresses, else return an empty signature
598
+ const orderedAttestations = orderAddresses.map(address => {
599
+ const addressString = address.toString();
600
+ return attestationMap.get(addressString)?.signature || Signature.empty();
601
+ });
602
+
603
+ return orderedAttestations;
604
+ }