@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.
- package/dest/global_variable_builder/global_builder.d.ts +2 -1
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +9 -7
- package/dest/publisher/l1-publisher.d.ts +22 -23
- package/dest/publisher/l1-publisher.d.ts.map +1 -1
- package/dest/publisher/l1-publisher.js +124 -67
- package/dest/sequencer/sequencer.d.ts +9 -1
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +120 -66
- package/package.json +19 -18
- package/src/global_variable_builder/global_builder.ts +8 -5
- package/src/publisher/l1-publisher.ts +147 -76
- package/src/sequencer/sequencer.ts +169 -88
|
@@ -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 {
|
|
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
|
|
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
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
//
|
|
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,
|
|
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(
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
|
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);
|