@aztec/sequencer-client 0.0.1-commit.96bb3f7 → 0.0.1-commit.9d2bcf6d
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/client/sequencer-client.js +1 -1
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +1 -3
- package/dest/global_variable_builder/global_builder.js +2 -2
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +1 -2
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +39 -18
- package/dest/sequencer/checkpoint_proposal_job.d.ts +32 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +130 -31
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -1
- package/dest/sequencer/metrics.d.ts +2 -2
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +27 -17
- package/dest/sequencer/sequencer.d.ts +19 -9
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +72 -12
- package/dest/test/mock_checkpoint_builder.d.ts +17 -13
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +28 -10
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +7 -7
- package/package.json +30 -28
- package/src/client/sequencer-client.ts +1 -1
- package/src/config.ts +1 -3
- package/src/global_variable_builder/global_builder.ts +2 -2
- package/src/index.ts +1 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +34 -18
- package/src/sequencer/checkpoint_proposal_job.ts +181 -51
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +36 -18
- package/src/sequencer/sequencer.ts +89 -11
- package/src/test/mock_checkpoint_builder.ts +64 -34
- package/src/test/utils.ts +19 -12
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -1,31 +1,40 @@
|
|
|
1
|
+
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
1
2
|
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
2
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BlockNumber,
|
|
6
|
+
CheckpointNumber,
|
|
7
|
+
EpochNumber,
|
|
8
|
+
IndexWithinCheckpoint,
|
|
9
|
+
SlotNumber,
|
|
10
|
+
} from '@aztec/foundation/branded-types';
|
|
4
11
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
5
12
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
13
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
14
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
15
|
import { filter } from '@aztec/foundation/iterator';
|
|
9
|
-
import type
|
|
16
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
10
17
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
11
18
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
12
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
19
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
13
20
|
import type { P2P } from '@aztec/p2p';
|
|
14
21
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
22
|
import {
|
|
16
23
|
CommitteeAttestation,
|
|
17
24
|
CommitteeAttestationsAndSigners,
|
|
18
|
-
|
|
25
|
+
L2Block,
|
|
19
26
|
type L2BlockSink,
|
|
27
|
+
type L2BlockSource,
|
|
20
28
|
MaliciousCommitteeAttestationsAndSigners,
|
|
21
29
|
} from '@aztec/stdlib/block';
|
|
22
30
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
23
31
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
24
32
|
import { Gas } from '@aztec/stdlib/gas';
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
import {
|
|
34
|
+
NoValidTxsError,
|
|
35
|
+
type PublicProcessorLimits,
|
|
36
|
+
type ResolvedSequencerConfig,
|
|
37
|
+
type WorldStateSynchronizer,
|
|
29
38
|
} from '@aztec/stdlib/interfaces/server';
|
|
30
39
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
31
40
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
@@ -35,6 +44,7 @@ import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
|
35
44
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
36
45
|
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
37
46
|
import { CheckpointBuilder, type FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
|
|
47
|
+
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
38
48
|
|
|
39
49
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
40
50
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
@@ -56,7 +66,10 @@ const TXS_POLLING_MS = 500;
|
|
|
56
66
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
57
67
|
*/
|
|
58
68
|
export class CheckpointProposalJob implements Traceable {
|
|
69
|
+
protected readonly log: Logger;
|
|
70
|
+
|
|
59
71
|
constructor(
|
|
72
|
+
private readonly epoch: EpochNumber,
|
|
60
73
|
private readonly slot: SlotNumber,
|
|
61
74
|
private readonly checkpointNumber: CheckpointNumber,
|
|
62
75
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
@@ -70,6 +83,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
70
83
|
private readonly p2pClient: P2P,
|
|
71
84
|
private readonly worldState: WorldStateSynchronizer,
|
|
72
85
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
86
|
+
private readonly l2BlockSource: L2BlockSource,
|
|
73
87
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
74
88
|
private readonly blockSink: L2BlockSink,
|
|
75
89
|
private readonly l1Constants: SequencerRollupConstants,
|
|
@@ -81,9 +95,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
81
95
|
private readonly metrics: SequencerMetrics,
|
|
82
96
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
83
97
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
84
|
-
protected readonly log: Logger,
|
|
85
98
|
public readonly tracer: Tracer,
|
|
86
|
-
|
|
99
|
+
bindings?: LoggerBindings,
|
|
100
|
+
) {
|
|
101
|
+
this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
|
|
102
|
+
}
|
|
87
103
|
|
|
88
104
|
/**
|
|
89
105
|
* Executes the checkpoint proposal job.
|
|
@@ -169,6 +185,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
169
185
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
170
186
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
171
187
|
|
|
188
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
189
|
+
const previousCheckpoints = (await this.l2BlockSource.getCheckpointsForEpoch(this.epoch)).filter(
|
|
190
|
+
c => c.number < this.checkpointNumber,
|
|
191
|
+
);
|
|
192
|
+
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
193
|
+
|
|
172
194
|
// Create a long-lived forked world state for the checkpoint builder
|
|
173
195
|
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
174
196
|
|
|
@@ -177,7 +199,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
177
199
|
this.checkpointNumber,
|
|
178
200
|
checkpointGlobalVariables,
|
|
179
201
|
l1ToL2Messages,
|
|
202
|
+
previousCheckpointOutHashes,
|
|
180
203
|
fork,
|
|
204
|
+
this.log.getBindings(),
|
|
181
205
|
);
|
|
182
206
|
|
|
183
207
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -191,13 +215,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
191
215
|
broadcastInvalidCheckpointProposal: this.config.broadcastInvalidBlockProposal,
|
|
192
216
|
};
|
|
193
217
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
218
|
+
let blocksInCheckpoint: L2Block[] = [];
|
|
219
|
+
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Main loop: build blocks for the checkpoint
|
|
223
|
+
const result = await this.buildBlocksForCheckpoint(
|
|
224
|
+
checkpointBuilder,
|
|
225
|
+
checkpointGlobalVariables.timestamp,
|
|
226
|
+
inHash,
|
|
227
|
+
blockProposalOptions,
|
|
228
|
+
);
|
|
229
|
+
blocksInCheckpoint = result.blocksInCheckpoint;
|
|
230
|
+
blockPendingBroadcast = result.blockPendingBroadcast;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
233
|
+
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
234
|
+
// which is normal for block building (may have picked different txs)
|
|
235
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
201
240
|
|
|
202
241
|
if (blocksInCheckpoint.length === 0) {
|
|
203
242
|
this.log.warn(`No blocks were built for slot ${this.slot}`, { slot: this.slot });
|
|
@@ -252,7 +291,22 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
252
291
|
|
|
253
292
|
// Proposer must sign over the attestations before pushing them to L1
|
|
254
293
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
255
|
-
|
|
294
|
+
let attestationsSignature: Signature;
|
|
295
|
+
try {
|
|
296
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
297
|
+
attestations,
|
|
298
|
+
signer,
|
|
299
|
+
this.slot,
|
|
300
|
+
this.checkpointNumber,
|
|
301
|
+
);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
// We shouldn't really get here since we yield to another HA node
|
|
304
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
305
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
256
310
|
|
|
257
311
|
// Enqueue publishing the checkpoint to L1
|
|
258
312
|
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
@@ -266,6 +320,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
266
320
|
|
|
267
321
|
return checkpoint;
|
|
268
322
|
} catch (err) {
|
|
323
|
+
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
324
|
+
// swallow this error. It's already been logged by a function deeper in the stack
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
269
328
|
this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
|
|
270
329
|
return undefined;
|
|
271
330
|
}
|
|
@@ -281,19 +340,22 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
281
340
|
inHash: Fr,
|
|
282
341
|
blockProposalOptions: BlockProposalOptions,
|
|
283
342
|
): Promise<{
|
|
284
|
-
blocksInCheckpoint:
|
|
285
|
-
blockPendingBroadcast: { block:
|
|
343
|
+
blocksInCheckpoint: L2Block[];
|
|
344
|
+
blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined;
|
|
286
345
|
}> {
|
|
287
|
-
const blocksInCheckpoint:
|
|
346
|
+
const blocksInCheckpoint: L2Block[] = [];
|
|
288
347
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
289
348
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
290
349
|
|
|
350
|
+
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
351
|
+
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
352
|
+
|
|
291
353
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
292
|
-
let blockPendingBroadcast: { block:
|
|
354
|
+
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
293
355
|
|
|
294
356
|
while (true) {
|
|
295
357
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
296
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
358
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
297
359
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
298
360
|
|
|
299
361
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
@@ -320,8 +382,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
320
382
|
blockNumber,
|
|
321
383
|
indexWithinCheckpoint,
|
|
322
384
|
txHashesAlreadyIncluded,
|
|
385
|
+
remainingBlobFields,
|
|
323
386
|
});
|
|
324
387
|
|
|
388
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
325
389
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
326
390
|
// If no block was produced due to not enough txs and this was the last subslot, exit
|
|
327
391
|
break;
|
|
@@ -344,13 +408,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
344
408
|
break;
|
|
345
409
|
}
|
|
346
410
|
|
|
347
|
-
const { block, usedTxs } = buildResult;
|
|
411
|
+
const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
|
|
348
412
|
blocksInCheckpoint.push(block);
|
|
349
413
|
|
|
414
|
+
// Update remaining blob fields for the next block
|
|
415
|
+
remainingBlobFields = newRemainingBlobFields;
|
|
416
|
+
|
|
350
417
|
// Sync the proposed block to the archiver to make it available
|
|
351
418
|
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
352
419
|
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
353
|
-
|
|
420
|
+
// Fire and forget - don't block the critical path, but log errors
|
|
421
|
+
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
422
|
+
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
354
426
|
|
|
355
427
|
// If this is the last block, exit the loop now so we start collecting attestations
|
|
356
428
|
if (timingInfo.isLastBlock) {
|
|
@@ -400,19 +472,27 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
400
472
|
|
|
401
473
|
/** Builds a single block. Called from the main block building loop. */
|
|
402
474
|
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
403
|
-
|
|
475
|
+
protected async buildSingleBlock(
|
|
404
476
|
checkpointBuilder: CheckpointBuilder,
|
|
405
477
|
opts: {
|
|
406
478
|
forceCreate?: boolean;
|
|
407
479
|
blockTimestamp: bigint;
|
|
408
480
|
blockNumber: BlockNumber;
|
|
409
|
-
indexWithinCheckpoint:
|
|
481
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
410
482
|
buildDeadline: Date | undefined;
|
|
411
483
|
txHashesAlreadyIncluded: Set<string>;
|
|
484
|
+
remainingBlobFields: number;
|
|
412
485
|
},
|
|
413
|
-
): Promise<{ block:
|
|
414
|
-
const {
|
|
415
|
-
|
|
486
|
+
): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
|
|
487
|
+
const {
|
|
488
|
+
blockTimestamp,
|
|
489
|
+
forceCreate,
|
|
490
|
+
blockNumber,
|
|
491
|
+
indexWithinCheckpoint,
|
|
492
|
+
buildDeadline,
|
|
493
|
+
txHashesAlreadyIncluded,
|
|
494
|
+
remainingBlobFields,
|
|
495
|
+
} = opts;
|
|
416
496
|
|
|
417
497
|
this.log.verbose(
|
|
418
498
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -445,46 +525,52 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
445
525
|
{ slot: this.slot, blockNumber, indexWithinCheckpoint },
|
|
446
526
|
);
|
|
447
527
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
528
|
+
|
|
529
|
+
// Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
|
|
530
|
+
const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
|
|
531
|
+
const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
|
|
532
|
+
|
|
448
533
|
const blockBuilderOptions: PublicProcessorLimits = {
|
|
449
534
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
450
535
|
maxBlockSize: this.config.maxBlockSizeInBytes,
|
|
451
536
|
maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
|
|
452
|
-
maxBlobFields:
|
|
537
|
+
maxBlobFields: maxBlobFieldsForTxs,
|
|
453
538
|
deadline: buildDeadline,
|
|
454
539
|
};
|
|
455
540
|
|
|
456
541
|
// Actually build the block by executing txs
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
542
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
543
|
+
checkpointBuilder,
|
|
544
|
+
pendingTxs,
|
|
545
|
+
blockNumber,
|
|
546
|
+
blockTimestamp,
|
|
547
|
+
blockBuilderOptions,
|
|
548
|
+
);
|
|
461
549
|
|
|
462
550
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
463
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
551
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
464
552
|
|
|
465
553
|
// Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
|
|
466
554
|
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
467
555
|
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
468
|
-
|
|
556
|
+
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
557
|
+
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
469
558
|
this.log.warn(
|
|
470
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed
|
|
471
|
-
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
|
|
559
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
560
|
+
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
|
|
472
561
|
);
|
|
473
|
-
this.eventEmitter.emit('block-
|
|
474
|
-
minTxs: minValidTxs,
|
|
475
|
-
availableTxs: numTxs,
|
|
476
|
-
slot: this.slot,
|
|
477
|
-
});
|
|
562
|
+
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
478
563
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
479
564
|
return undefined;
|
|
480
565
|
}
|
|
481
566
|
|
|
482
567
|
// Block creation succeeded, emit stats and metrics
|
|
568
|
+
const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
|
|
569
|
+
|
|
483
570
|
const blockStats = {
|
|
484
571
|
eventName: 'l2-block-built',
|
|
485
572
|
duration: blockBuildDuration,
|
|
486
573
|
publicProcessDuration: publicProcessorDuration,
|
|
487
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
488
574
|
...block.getStats(),
|
|
489
575
|
} satisfies L2BlockBuiltStats;
|
|
490
576
|
|
|
@@ -500,7 +586,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
500
586
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
501
587
|
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
502
588
|
|
|
503
|
-
return { block, usedTxs };
|
|
589
|
+
return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
|
|
504
590
|
} catch (err: any) {
|
|
505
591
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
506
592
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -510,17 +596,40 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
510
596
|
}
|
|
511
597
|
}
|
|
512
598
|
|
|
599
|
+
/** Uses the checkpoint builder to build a block, catching specific txs */
|
|
600
|
+
private async buildSingleBlockWithCheckpointBuilder(
|
|
601
|
+
checkpointBuilder: CheckpointBuilder,
|
|
602
|
+
pendingTxs: AsyncIterable<Tx>,
|
|
603
|
+
blockNumber: BlockNumber,
|
|
604
|
+
blockTimestamp: bigint,
|
|
605
|
+
blockBuilderOptions: PublicProcessorLimits,
|
|
606
|
+
) {
|
|
607
|
+
try {
|
|
608
|
+
const workTimer = new Timer();
|
|
609
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
610
|
+
const blockBuildDuration = workTimer.ms();
|
|
611
|
+
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
612
|
+
} catch (err: unknown) {
|
|
613
|
+
if (isErrorClass(err, NoValidTxsError)) {
|
|
614
|
+
return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
|
|
615
|
+
}
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
513
620
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
514
621
|
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
515
622
|
private async waitForMinTxs(opts: {
|
|
516
623
|
forceCreate?: boolean;
|
|
517
624
|
blockNumber: BlockNumber;
|
|
518
|
-
indexWithinCheckpoint:
|
|
625
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
519
626
|
buildDeadline: Date | undefined;
|
|
520
627
|
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
521
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
522
628
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
523
629
|
|
|
630
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
631
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
632
|
+
|
|
524
633
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
525
634
|
const startBuildingDeadline = buildDeadline
|
|
526
635
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -582,7 +691,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
582
691
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
583
692
|
? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
|
|
584
693
|
: this.l1Constants.slotDuration;
|
|
585
|
-
const attestationDeadline = new Date(this.
|
|
694
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
586
695
|
|
|
587
696
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
588
697
|
|
|
@@ -678,8 +787,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
678
787
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
679
788
|
* would never receive its own block without this explicit sync.
|
|
680
789
|
*/
|
|
681
|
-
private async syncProposedBlockToArchiver(block:
|
|
682
|
-
// TODO(palla/mbps): Change default to false once block sync is stable.
|
|
790
|
+
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
683
791
|
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
684
792
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
685
793
|
blockNumber: block.number,
|
|
@@ -719,6 +827,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
719
827
|
this.publisher.clearPendingRequests();
|
|
720
828
|
}
|
|
721
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
832
|
+
*/
|
|
833
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
834
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
835
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
836
|
+
slot: this.slot,
|
|
837
|
+
signedByNode: err.signedByNode,
|
|
838
|
+
});
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
if (err instanceof SlashingProtectionError) {
|
|
842
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
843
|
+
slot: this.slot,
|
|
844
|
+
existingMessageHash: err.existingMessageHash,
|
|
845
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
846
|
+
});
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
|
|
722
852
|
/** Waits until a specific time within the current slot */
|
|
723
853
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
724
854
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -5,6 +5,8 @@ import type { SlasherClientInterface } from '@aztec/slasher';
|
|
|
5
5
|
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
6
6
|
import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
7
7
|
import type { ValidatorClient } from '@aztec/validator-client';
|
|
8
|
+
import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
|
|
9
|
+
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
8
10
|
|
|
9
11
|
import type { TypedDataDefinition } from 'viem';
|
|
10
12
|
|
|
@@ -17,7 +19,8 @@ import type { SequencerRollupConstants } from './types.js';
|
|
|
17
19
|
*/
|
|
18
20
|
export class CheckpointVoter {
|
|
19
21
|
private slotTimestamp: bigint;
|
|
20
|
-
private
|
|
22
|
+
private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
23
|
+
private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
21
24
|
|
|
22
25
|
constructor(
|
|
23
26
|
private readonly slot: SlotNumber,
|
|
@@ -31,8 +34,16 @@ export class CheckpointVoter {
|
|
|
31
34
|
private readonly log: Logger,
|
|
32
35
|
) {
|
|
33
36
|
this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
// Create separate signers with appropriate duty contexts for governance and slashing votes
|
|
39
|
+
// These use HA protection to ensure only one node signs per slot/duty
|
|
40
|
+
const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
|
|
41
|
+
this.governanceSigner = (msg: TypedDataDefinition) =>
|
|
42
|
+
this.validatorClient.signWithAddress(this.attestorAddress, msg, governanceContext).then(s => s.toString());
|
|
43
|
+
|
|
44
|
+
const slashingContext: SigningContext = { slot: this.slot, dutyType: DutyType.SLASHING_VOTE };
|
|
45
|
+
this.slashingSigner = (msg: TypedDataDefinition) =>
|
|
46
|
+
this.validatorClient.signWithAddress(this.attestorAddress, msg, slashingContext).then(s => s.toString());
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/**
|
|
@@ -68,10 +79,17 @@ export class CheckpointVoter {
|
|
|
68
79
|
this.slot,
|
|
69
80
|
this.slotTimestamp,
|
|
70
81
|
this.attestorAddress,
|
|
71
|
-
this.
|
|
82
|
+
this.governanceSigner,
|
|
72
83
|
);
|
|
73
84
|
} catch (err) {
|
|
74
|
-
|
|
85
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
86
|
+
this.log.info(`Governance vote already signed by another node`, {
|
|
87
|
+
slot: this.slot,
|
|
88
|
+
signedByNode: err.signedByNode,
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
this.log.error(`Error enqueueing governance vote`, err);
|
|
92
|
+
}
|
|
75
93
|
return false;
|
|
76
94
|
}
|
|
77
95
|
}
|
|
@@ -95,10 +113,17 @@ export class CheckpointVoter {
|
|
|
95
113
|
this.slot,
|
|
96
114
|
this.slotTimestamp,
|
|
97
115
|
this.attestorAddress,
|
|
98
|
-
this.
|
|
116
|
+
this.slashingSigner,
|
|
99
117
|
);
|
|
100
118
|
} catch (err) {
|
|
101
|
-
|
|
119
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
120
|
+
this.log.info(`Slashing vote already signed by another node`, {
|
|
121
|
+
slot: this.slot,
|
|
122
|
+
signedByNode: err.signedByNode,
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
this.log.error(`Error enqueueing slashing vote`, err);
|
|
126
|
+
}
|
|
102
127
|
return false;
|
|
103
128
|
}
|
|
104
129
|
}
|
package/src/sequencer/index.ts
CHANGED
package/src/sequencer/metrics.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type TelemetryClient,
|
|
12
12
|
type Tracer,
|
|
13
13
|
type UpDownCounter,
|
|
14
|
+
createUpDownCounterWithDefault,
|
|
14
15
|
} from '@aztec/telemetry-client';
|
|
15
16
|
|
|
16
17
|
import { type Hex, formatUnits } from 'viem';
|
|
@@ -67,7 +68,9 @@ export class SequencerMetrics {
|
|
|
67
68
|
this.meter = client.getMeter(name);
|
|
68
69
|
this.tracer = client.getTracer(name);
|
|
69
70
|
|
|
70
|
-
this.blockCounter = this.meter
|
|
71
|
+
this.blockCounter = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_BLOCK_COUNT, {
|
|
72
|
+
[Attributes.STATUS]: ['failed', 'built'],
|
|
73
|
+
});
|
|
71
74
|
|
|
72
75
|
this.blockBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_BUILD_DURATION);
|
|
73
76
|
|
|
@@ -77,23 +80,15 @@ export class SequencerMetrics {
|
|
|
77
80
|
|
|
78
81
|
this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
|
|
79
82
|
|
|
80
|
-
// Init gauges and counters
|
|
81
|
-
this.blockCounter.add(0, {
|
|
82
|
-
[Attributes.STATUS]: 'failed',
|
|
83
|
-
});
|
|
84
|
-
this.blockCounter.add(0, {
|
|
85
|
-
[Attributes.STATUS]: 'built',
|
|
86
|
-
});
|
|
87
|
-
|
|
88
83
|
this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_BLOCK_REWARDS);
|
|
89
84
|
|
|
90
|
-
this.slots = this.meter
|
|
85
|
+
this.slots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLOT_COUNT);
|
|
91
86
|
|
|
92
87
|
/**
|
|
93
88
|
* NOTE: we do not track missed slots as a separate metric. That would be difficult to determine
|
|
94
89
|
* Instead, use a computed metric, `slots - filledSlots` to get the number of slots a sequencer has missed.
|
|
95
90
|
*/
|
|
96
|
-
this.filledSlots = this.meter
|
|
91
|
+
this.filledSlots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_FILLED_SLOT_COUNT);
|
|
97
92
|
|
|
98
93
|
this.timeToCollectAttestations = this.meter.createGauge(Metrics.SEQUENCER_COLLECT_ATTESTATIONS_DURATION);
|
|
99
94
|
|
|
@@ -103,20 +98,41 @@ export class SequencerMetrics {
|
|
|
103
98
|
|
|
104
99
|
this.collectedAttestions = this.meter.createGauge(Metrics.SEQUENCER_COLLECTED_ATTESTATIONS_COUNT);
|
|
105
100
|
|
|
106
|
-
this.blockProposalFailed =
|
|
101
|
+
this.blockProposalFailed = createUpDownCounterWithDefault(
|
|
102
|
+
this.meter,
|
|
103
|
+
Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT,
|
|
104
|
+
);
|
|
107
105
|
|
|
108
|
-
this.blockProposalSuccess =
|
|
106
|
+
this.blockProposalSuccess = createUpDownCounterWithDefault(
|
|
107
|
+
this.meter,
|
|
108
|
+
Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT,
|
|
109
|
+
);
|
|
109
110
|
|
|
110
|
-
this.checkpointSuccess = this.meter
|
|
111
|
+
this.checkpointSuccess = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
|
|
111
112
|
|
|
112
|
-
this.blockProposalPrecheckFailed =
|
|
113
|
+
this.blockProposalPrecheckFailed = createUpDownCounterWithDefault(
|
|
114
|
+
this.meter,
|
|
113
115
|
Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
|
|
116
|
+
{
|
|
117
|
+
[Attributes.ERROR_TYPE]: [
|
|
118
|
+
'slot_already_taken',
|
|
119
|
+
'rollup_contract_check_failed',
|
|
120
|
+
'slot_mismatch',
|
|
121
|
+
'block_number_mismatch',
|
|
122
|
+
],
|
|
123
|
+
},
|
|
114
124
|
);
|
|
115
125
|
|
|
116
|
-
this.slashingAttempts = this.meter
|
|
126
|
+
this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
|
|
117
127
|
|
|
118
128
|
// Fisherman fee analysis metrics
|
|
119
|
-
this.fishermanWouldBeIncluded =
|
|
129
|
+
this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
|
|
130
|
+
this.meter,
|
|
131
|
+
Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED,
|
|
132
|
+
{
|
|
133
|
+
[Attributes.OK]: [true, false],
|
|
134
|
+
},
|
|
135
|
+
);
|
|
120
136
|
|
|
121
137
|
this.fishermanTimeBeforeBlock = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_TIME_BEFORE_BLOCK);
|
|
122
138
|
|
|
@@ -231,7 +247,9 @@ export class SequencerMetrics {
|
|
|
231
247
|
this.blockProposalSuccess.add(1);
|
|
232
248
|
}
|
|
233
249
|
|
|
234
|
-
recordBlockProposalPrecheckFailed(
|
|
250
|
+
recordBlockProposalPrecheckFailed(
|
|
251
|
+
checkType: 'slot_already_taken' | 'rollup_contract_check_failed' | 'slot_mismatch' | 'block_number_mismatch',
|
|
252
|
+
) {
|
|
235
253
|
this.blockProposalPrecheckFailed.add(1, {
|
|
236
254
|
[Attributes.ERROR_TYPE]: checkType,
|
|
237
255
|
});
|