@aztec/sequencer-client 5.0.0-rc.2 → 5.0.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.
- package/dest/client/sequencer-client.d.ts +16 -3
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +17 -3
- package/dest/config.d.ts +2 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +7 -2
- package/dest/global_variable_builder/fee_predictor.d.ts +1 -1
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -1
- package/dest/global_variable_builder/fee_predictor.js +11 -1
- package/dest/global_variable_builder/fee_provider.d.ts +1 -1
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -1
- package/dest/global_variable_builder/fee_provider.js +15 -2
- package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
- package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
- package/dest/publisher/l1_to_l2_messaging.js +70 -0
- package/dest/publisher/sequencer-publisher.d.ts +1 -1
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +6 -2
- package/dest/publisher/write_json.d.ts +11 -0
- package/dest/publisher/write_json.d.ts.map +1 -0
- package/dest/publisher/write_json.js +57 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +1 -1
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +11 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts +4 -6
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +27 -24
- package/dest/sequencer/events.d.ts +1 -2
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/requests_tracker.d.ts +22 -0
- package/dest/sequencer/requests_tracker.d.ts.map +1 -0
- package/dest/sequencer/requests_tracker.js +33 -0
- package/dest/sequencer/sequencer.d.ts +39 -4
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +81 -21
- package/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +1 -0
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +19 -3
- package/src/config.ts +7 -2
- package/src/global_variable_builder/fee_predictor.ts +11 -1
- package/src/global_variable_builder/fee_provider.ts +19 -2
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/sequencer-publisher.ts +8 -2
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/automine_sequencer.ts +11 -10
- package/src/sequencer/checkpoint_proposal_job.ts +40 -29
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +83 -15
- package/src/test/utils.ts +1 -0
|
@@ -362,8 +362,7 @@ export class AutomineSequencer {
|
|
|
362
362
|
return;
|
|
363
363
|
}
|
|
364
364
|
try {
|
|
365
|
-
|
|
366
|
-
if (pending > 0) {
|
|
365
|
+
if (await this.deps.p2pClient.hasEligiblePendingTxs(1)) {
|
|
367
366
|
// Fire-and-forget; the build result is delivered via `buildIfPending()` callers,
|
|
368
367
|
// not via the poller.
|
|
369
368
|
void this.buildIfPending().catch(err => {
|
|
@@ -389,13 +388,13 @@ export class AutomineSequencer {
|
|
|
389
388
|
}
|
|
390
389
|
await this.reconcileDateProvider();
|
|
391
390
|
|
|
392
|
-
|
|
393
|
-
// For mempool-driven builds, wait for at least `minTxsPerBlock` pending txs (or 1 if not set)
|
|
391
|
+
// For mempool-driven builds, wait for at least `minTxsPerBlock` age-eligible txs (or 1 if not set)
|
|
394
392
|
// before building. This mirrors the production sequencer's `waitForMinTxs` behavior, and is
|
|
395
393
|
// required for tests that bundle multiple txs into one block via `setConfig({ minTxsPerBlock })`.
|
|
396
|
-
// Explicit empty-block / warp paths pass `allowEmpty: true
|
|
394
|
+
// Explicit empty-block / warp paths pass `allowEmpty: true`, giving minRequired 0, which
|
|
395
|
+
// hasEligiblePendingTxs treats as always satisfied so the gate is bypassed.
|
|
397
396
|
const minRequired = allowEmpty ? 0 : Math.max(this.deps.config.minTxsPerBlock ?? 1, 1);
|
|
398
|
-
if (
|
|
397
|
+
if (!(await this.deps.p2pClient.hasEligiblePendingTxs(minRequired))) {
|
|
399
398
|
return undefined;
|
|
400
399
|
}
|
|
401
400
|
|
|
@@ -439,7 +438,6 @@ export class AutomineSequencer {
|
|
|
439
438
|
blockNumber: nextBlockNumber,
|
|
440
439
|
slot: targetSlot,
|
|
441
440
|
slotTimestamp: slotBoundaryTs,
|
|
442
|
-
txCount,
|
|
443
441
|
allowEmpty,
|
|
444
442
|
});
|
|
445
443
|
|
|
@@ -717,9 +715,12 @@ export class AutomineSequencer {
|
|
|
717
715
|
await rollupCheatCodes.markAsProven(target);
|
|
718
716
|
// Settlement is a direct L1 storage write that mines no block, unlike a real epoch proof landing
|
|
719
717
|
// on L1. The archiver's L1 sync short-circuits while the L1 block hash is unchanged, so it would
|
|
720
|
-
// never re-read the proven tip until the next build/warp mines a block. Mine one
|
|
721
|
-
//
|
|
722
|
-
|
|
718
|
+
// never re-read the proven tip until the next build/warp mines a block. Mine one L1 block so the
|
|
719
|
+
// block hash advances, then force an immediate sync that observes the new proven checkpoint. Use
|
|
720
|
+
// evmMine (not mineEmptyBlock): mineEmptyBlock drops+re-adds the mempool non-atomically, which can
|
|
721
|
+
// silently lose a test's concurrently-submitted direct L1 tx. evm_mine just includes whatever is
|
|
722
|
+
// pending, which is fine here — we only need the block hash to advance.
|
|
723
|
+
await this.deps.ethCheatCodes.evmMine();
|
|
723
724
|
await this.deps.archiver.syncImmediate();
|
|
724
725
|
|
|
725
726
|
this.log.verbose(`Proved up to checkpoint ${target}`, { target, proven, startEpoch, endEpoch });
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type L2BlockSink,
|
|
32
32
|
type L2BlockSource,
|
|
33
33
|
MaliciousCommitteeAttestationsAndSigners,
|
|
34
|
+
MaliciousYParityCommitteeAttestationsAndSigners,
|
|
34
35
|
type ProposedCheckpointSink,
|
|
35
36
|
type ValidateCheckpointResult,
|
|
36
37
|
} from '@aztec/stdlib/block';
|
|
@@ -74,6 +75,7 @@ import { CheckpointVoter } from './checkpoint_voter.js';
|
|
|
74
75
|
import { SequencerInterruptedError } from './errors.js';
|
|
75
76
|
import type { SequencerEvents } from './events.js';
|
|
76
77
|
import type { SequencerMetrics } from './metrics.js';
|
|
78
|
+
import type { RequestsTracker } from './requests_tracker.js';
|
|
77
79
|
import type { SequencerRollupConstants } from './types.js';
|
|
78
80
|
import { SequencerState } from './utils.js';
|
|
79
81
|
|
|
@@ -105,8 +107,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
105
107
|
protected readonly log: Logger;
|
|
106
108
|
private readonly checkpointEventLog: Logger;
|
|
107
109
|
|
|
108
|
-
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
109
|
-
private pendingL1Submission: Promise<void> | undefined;
|
|
110
110
|
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
111
111
|
private interrupted = false;
|
|
112
112
|
|
|
@@ -151,6 +151,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
151
151
|
private readonly metrics: SequencerMetrics,
|
|
152
152
|
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
153
153
|
protected readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
154
|
+
// Shared with the owning sequencer, which drains it during shutdown; the fire-and-forget L1
|
|
155
|
+
// submission this job backgrounds is tracked here rather than in a job-local tracker.
|
|
156
|
+
protected readonly pendingRequests: RequestsTracker,
|
|
154
157
|
private readonly setStateFn: (state: SequencerState, slot: SlotNumber) => void,
|
|
155
158
|
public readonly tracer: Tracer,
|
|
156
159
|
bindings?: LoggerBindings,
|
|
@@ -183,12 +186,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
183
186
|
this.setStateFn(state, this.targetSlot);
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
187
|
-
public async awaitPendingSubmission(): Promise<void> {
|
|
188
|
-
this.log.info('Awaiting pending L1 payload submission');
|
|
189
|
-
await this.pendingL1Submission;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
189
|
/** Interrupts job-owned waits, including the publisher's send-at-slot sleep, so shutdown can finish. */
|
|
193
190
|
public interrupt(): void {
|
|
194
191
|
this.interrupted = true;
|
|
@@ -257,7 +254,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
257
254
|
// signature verification to fail silently inside Multicall3. Delay submission to the
|
|
258
255
|
// start of `targetSlot` so the tx mines in the slot the vote was signed for.
|
|
259
256
|
if (!this.config.fishermanMode) {
|
|
260
|
-
this.
|
|
257
|
+
this.pendingRequests.trackRequest(this.publisher.sendRequestsAt(this.targetSlot), () => this.interrupt());
|
|
261
258
|
}
|
|
262
259
|
return undefined;
|
|
263
260
|
}
|
|
@@ -272,7 +269,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
272
269
|
}
|
|
273
270
|
|
|
274
271
|
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
275
|
-
this.
|
|
272
|
+
this.pendingRequests.trackRequest(this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises), () =>
|
|
273
|
+
this.interrupt(),
|
|
274
|
+
);
|
|
276
275
|
|
|
277
276
|
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
278
277
|
return checkpoint;
|
|
@@ -280,7 +279,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
280
279
|
|
|
281
280
|
/**
|
|
282
281
|
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
283
|
-
* Runs as a fire-and-forget task
|
|
282
|
+
* Runs as a fire-and-forget task tracked in the sequencer's shared tracker so the work loop is unblocked.
|
|
284
283
|
*/
|
|
285
284
|
private async waitForAttestationsAndEnqueueSubmissionAsync(
|
|
286
285
|
broadcast: CheckpointProposalBroadcast,
|
|
@@ -1049,7 +1048,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1049
1048
|
|
|
1050
1049
|
try {
|
|
1051
1050
|
// Wait until we have enough txs to build the block
|
|
1052
|
-
const {
|
|
1051
|
+
const { canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
1053
1052
|
if (!canStartBuilding) {
|
|
1054
1053
|
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1055
1054
|
reason: 'insufficient_txs',
|
|
@@ -1057,22 +1056,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1057
1056
|
slot: this.targetSlot,
|
|
1058
1057
|
checkpointNumber: this.checkpointNumber,
|
|
1059
1058
|
indexWithinCheckpoint,
|
|
1060
|
-
availableTxs,
|
|
1061
1059
|
minTxs,
|
|
1062
1060
|
});
|
|
1063
1061
|
this.log.verbose(
|
|
1064
|
-
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (
|
|
1062
|
+
`Not enough age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (needs ${minTxs} eligible)`,
|
|
1065
1063
|
{
|
|
1066
1064
|
reason: 'insufficient_txs',
|
|
1067
1065
|
blockNumber,
|
|
1068
1066
|
slot: this.targetSlot,
|
|
1069
1067
|
checkpointNumber: this.checkpointNumber,
|
|
1070
1068
|
indexWithinCheckpoint,
|
|
1071
|
-
availableTxs,
|
|
1072
1069
|
minTxs,
|
|
1073
1070
|
},
|
|
1074
1071
|
);
|
|
1075
|
-
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs,
|
|
1072
|
+
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, slot: this.targetSlot });
|
|
1076
1073
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
1077
1074
|
return { failure: 'insufficient-txs' };
|
|
1078
1075
|
}
|
|
@@ -1086,10 +1083,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1086
1083
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
1087
1084
|
);
|
|
1088
1085
|
|
|
1089
|
-
this.log.debug(
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1086
|
+
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot}`, {
|
|
1087
|
+
slot: this.targetSlot,
|
|
1088
|
+
blockNumber,
|
|
1089
|
+
indexWithinCheckpoint,
|
|
1090
|
+
});
|
|
1093
1091
|
this.setState(SequencerState.CREATING_BLOCK);
|
|
1094
1092
|
|
|
1095
1093
|
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
@@ -1241,7 +1239,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1241
1239
|
blockNumber: BlockNumber;
|
|
1242
1240
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
1243
1241
|
buildDeadline: Date | undefined;
|
|
1244
|
-
}): Promise<{ canStartBuilding: boolean;
|
|
1242
|
+
}): Promise<{ canStartBuilding: boolean; minTxs: number }> {
|
|
1245
1243
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
1246
1244
|
|
|
1247
1245
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -1252,26 +1250,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1252
1250
|
? new Date(buildDeadline.getTime() - this.timetable.minBlockDuration * 1000)
|
|
1253
1251
|
: undefined;
|
|
1254
1252
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
while (!forceCreate &&
|
|
1253
|
+
// Gate on age-eligible txs so we don't start building on txs the builder would then filter out for being
|
|
1254
|
+
// too fresh. hasEligiblePendingTxs early-exits once minTxs are eligible instead of counting the whole pool.
|
|
1255
|
+
while (!forceCreate && !(await this.p2pClient.hasEligiblePendingTxs(minTxs))) {
|
|
1258
1256
|
// If we're past deadline, or we have no deadline, give up
|
|
1259
1257
|
const now = this.dateProvider.nowAsDate();
|
|
1260
1258
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
1261
|
-
return { canStartBuilding: false,
|
|
1259
|
+
return { canStartBuilding: false, minTxs };
|
|
1262
1260
|
}
|
|
1263
1261
|
|
|
1264
1262
|
// Wait a bit before checking again
|
|
1265
1263
|
this.setState(SequencerState.WAITING_FOR_TXS);
|
|
1266
1264
|
this.log.verbose(
|
|
1267
|
-
`Waiting for
|
|
1268
|
-
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
1265
|
+
`Waiting for ${minTxs} age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot}`,
|
|
1266
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint, minTxs },
|
|
1269
1267
|
);
|
|
1270
1268
|
await this.waitForTxsPollingInterval();
|
|
1271
|
-
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
1272
1269
|
}
|
|
1273
1270
|
|
|
1274
|
-
return { canStartBuilding: true,
|
|
1271
|
+
return { canStartBuilding: true, minTxs };
|
|
1275
1272
|
}
|
|
1276
1273
|
|
|
1277
1274
|
private async getSignedCommitteeAttestations(
|
|
@@ -1387,6 +1384,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1387
1384
|
this.config.injectFakeAttestation ||
|
|
1388
1385
|
this.config.injectHighSValueAttestation ||
|
|
1389
1386
|
this.config.injectUnrecoverableSignatureAttestation ||
|
|
1387
|
+
this.config.injectYParityAttestation ||
|
|
1390
1388
|
this.config.shuffleAttestationOrdering
|
|
1391
1389
|
) {
|
|
1392
1390
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
@@ -1488,6 +1486,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1488
1486
|
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1489
1487
|
}
|
|
1490
1488
|
|
|
1489
|
+
if (this.config.injectYParityAttestation) {
|
|
1490
|
+
// Force every non-proposer signed slot's recovery byte to yParity (v ∈ {0, 1}) form in the packed L1
|
|
1491
|
+
// tuple, after packAttestations has canonicalized it. The proposer's own slot is left canonical so
|
|
1492
|
+
// propose() still passes verifyProposer. Models a malicious proposer landing a checkpoint L1 accepts
|
|
1493
|
+
// but that can never be proven (ECDSA.recover rejects v ∉ {27, 28}).
|
|
1494
|
+
this.log.warn(`Injecting yParity attestations in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
1495
|
+
return new MaliciousYParityCommitteeAttestationsAndSigners(
|
|
1496
|
+
attestations,
|
|
1497
|
+
proposerIndex,
|
|
1498
|
+
this.getSignatureContext(),
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1491
1502
|
if (this.config.shuffleAttestationOrdering) {
|
|
1492
1503
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
1493
1504
|
|
package/src/sequencer/events.ts
CHANGED
|
@@ -49,7 +49,7 @@ export type SequencerEvents = {
|
|
|
49
49
|
simulatedPending: CheckpointNumber | undefined;
|
|
50
50
|
}) => void;
|
|
51
51
|
['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
52
|
-
['block-tx-count-check-failed']: (args: { minTxs: number;
|
|
52
|
+
['block-tx-count-check-failed']: (args: { minTxs: number; slot: SlotNumber }) => void;
|
|
53
53
|
['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
54
54
|
['block-proposed']: (args: {
|
|
55
55
|
blockNumber: BlockNumber;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** A tracked request: the awaitable to wait on, plus an optional interrupt to cancel it early. */
|
|
2
|
+
type TrackedRequest = { promise: Promise<unknown>; interrupt?: () => void };
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tracks fire-and-forget requests so a shutdown path can interrupt any in-flight work and await it to
|
|
6
|
+
* settle. Each tracked request is a promise plus an optional interrupt callback; the entry removes itself
|
|
7
|
+
* from the bag once its promise settles, so the tracker only ever holds requests that are still in flight.
|
|
8
|
+
*/
|
|
9
|
+
export class RequestsTracker {
|
|
10
|
+
private readonly requests = new Map<number, TrackedRequest>();
|
|
11
|
+
private nextId = 0;
|
|
12
|
+
|
|
13
|
+
/** Number of in-flight requests currently tracked. */
|
|
14
|
+
public get size(): number {
|
|
15
|
+
return this.requests.size;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tracks a fire-and-forget request. `interrupt`, when provided, is invoked by {@link interruptRequests}
|
|
20
|
+
* to cancel the in-flight work so its promise settles promptly. The entry auto-removes once `promise`
|
|
21
|
+
* settles, whether it resolves or rejects.
|
|
22
|
+
*/
|
|
23
|
+
public trackRequest(promise: Promise<unknown>, interrupt?: () => void): void {
|
|
24
|
+
const id = this.nextId++;
|
|
25
|
+
const settled = promise.then(
|
|
26
|
+
() => this.requests.delete(id),
|
|
27
|
+
() => this.requests.delete(id),
|
|
28
|
+
);
|
|
29
|
+
this.requests.set(id, { promise: settled, interrupt });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Interrupts every in-flight request that was tracked with an interrupt callback. */
|
|
33
|
+
public interruptRequests(): void {
|
|
34
|
+
for (const { interrupt } of this.requests.values()) {
|
|
35
|
+
interrupt?.();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Awaits every currently in-flight request to settle. */
|
|
40
|
+
public async awaitRequests(): Promise<void> {
|
|
41
|
+
await Promise.all([...this.requests.values()].map(request => request.promise));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -55,6 +55,7 @@ import { CheckpointVoter } from './checkpoint_voter.js';
|
|
|
55
55
|
import { SequencerInterruptedError } from './errors.js';
|
|
56
56
|
import type { SequencerEvents } from './events.js';
|
|
57
57
|
import { SequencerMetrics } from './metrics.js';
|
|
58
|
+
import { RequestsTracker } from './requests_tracker.js';
|
|
58
59
|
import type { SequencerRollupConstants } from './types.js';
|
|
59
60
|
import { SequencerState } from './utils.js';
|
|
60
61
|
|
|
@@ -79,7 +80,7 @@ type SequencerSlotContext = {
|
|
|
79
80
|
* - Votes for proposals and slashes on L1
|
|
80
81
|
*/
|
|
81
82
|
export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
|
|
82
|
-
|
|
83
|
+
protected runningPromise?: RunningPromise;
|
|
83
84
|
private state = SequencerState.STOPPED;
|
|
84
85
|
private stateSlotNumber: SlotNumber | undefined;
|
|
85
86
|
/** Wall-clock time (ms, via the date provider) at which the current state was entered. */
|
|
@@ -110,6 +111,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
110
111
|
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
111
112
|
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* In-flight fire-and-forget requests that {@link stop} interrupts and drains, and {@link pause} awaits
|
|
116
|
+
* untouched: the checkpoint proposal jobs' backgrounded L1 submissions (each job is handed this shared
|
|
117
|
+
* tracker) plus the sequencer's own fallback submissions (votes/prune when we cannot build, or
|
|
118
|
+
* escape-hatch votes). Each fallback send is gated by its wrapper publisher's interruptible target-slot
|
|
119
|
+
* sleep; the tracked interrupt wakes that sleep so the send short-circuits without publishing. A wrapper
|
|
120
|
+
* is created for a single send and is never restarted, so once interrupted its sleeper can never publish
|
|
121
|
+
* a stale-slot tx, even after the pooled publishers are restarted by a later {@link start}.
|
|
122
|
+
*/
|
|
123
|
+
protected readonly pendingRequests = new RequestsTracker();
|
|
124
|
+
|
|
113
125
|
/** Proposer schedule and block sub-slot timetable for the sequencer, rebuilt on every config update. */
|
|
114
126
|
protected timetable!: ProposerTimetable;
|
|
115
127
|
|
|
@@ -273,8 +285,22 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
273
285
|
getKzg();
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Starts (or restarts) the sequencer and moves it to the IDLE state. Idempotent: a start while already
|
|
290
|
+
* running is a no-op, so it never orphans the previous poll loop. A start while STOPPING throws, since the
|
|
291
|
+
* in-flight stop would mark the fresh loop's state STOPPED and orphan it — silently doing nothing would
|
|
292
|
+
* leave the caller believing the sequencer is running when it is on its way to STOPPED. Safe to call after
|
|
293
|
+
* a previous {@link stop} has resolved; the caller must also restart the publishers (see
|
|
294
|
+
* {@link SequencerClient.start}) so L1 publishing is re-enabled.
|
|
295
|
+
*/
|
|
277
296
|
public start() {
|
|
297
|
+
if (this.state === SequencerState.STOPPING) {
|
|
298
|
+
throw new Error('Cannot start sequencer while it is stopping');
|
|
299
|
+
}
|
|
300
|
+
if (this.runningPromise?.isRunning()) {
|
|
301
|
+
this.log.warn('Attempted to start sequencer that is already running');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
278
304
|
this.runningPromise = new RunningPromise(
|
|
279
305
|
this.safeWork.bind(this),
|
|
280
306
|
this.log,
|
|
@@ -290,18 +316,56 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
290
316
|
return this.runningPromise?.trigger();
|
|
291
317
|
}
|
|
292
318
|
|
|
293
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* Stops the sequencer from building blocks and moves it to STOPPED state, interrupting the in-flight
|
|
321
|
+
* work() iteration and its pending L1 submissions for a fast shutdown, then draining before returning.
|
|
322
|
+
* Idempotent: a second call while already stopped or stopping is a no-op. Lifecycle calls are expected
|
|
323
|
+
* to be serialized by the caller. For a restartable pause that lets in-flight work finish untouched
|
|
324
|
+
* (e.g. around a test clock warp), use {@link pause} instead.
|
|
325
|
+
*/
|
|
294
326
|
public async stop(): Promise<void> {
|
|
327
|
+
if (this.state === SequencerState.STOPPED || this.state === SequencerState.STOPPING) {
|
|
328
|
+
this.log.debug(`Sequencer already ${this.state.toLowerCase()}, ignoring stop`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
295
331
|
this.log.info(`Stopping sequencer`);
|
|
296
332
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
297
333
|
this.lastCheckpointProposalJob?.interrupt();
|
|
298
334
|
await this.publisherFactory.stopAll();
|
|
335
|
+
// Stop the poll loop and await the in-flight work() iteration. work() registers its fire-and-forget
|
|
336
|
+
// requests synchronously before returning, so once the loop has stopped, pendingRequests holds every
|
|
337
|
+
// request that will ever exist. Interrupting any earlier could miss a request registered by the last
|
|
338
|
+
// in-flight iteration.
|
|
299
339
|
await this.runningPromise?.stop();
|
|
300
|
-
|
|
340
|
+
this.pendingRequests.interruptRequests();
|
|
341
|
+
await this.pendingRequests.awaitRequests();
|
|
301
342
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
302
343
|
this.log.info('Stopped sequencer');
|
|
303
344
|
}
|
|
304
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Gracefully pauses block production so the sequencer can later be resumed with {@link start}: halts the
|
|
348
|
+
* poll loop and waits for the in-flight work() iteration and every pending L1 submission / fallback send
|
|
349
|
+
* to finish, without interrupting them. No interrupt lands mid-build, so no spurious checkpoint-error is
|
|
350
|
+
* emitted and no enqueued checkpoint is dropped. Deliberately does not stop inner services (validator/HA
|
|
351
|
+
* signer, publishers), so the slashing-protection store stays open and the sequencer stays restartable.
|
|
352
|
+
* Used by tests that pause sequencers around an L1 clock warp. Idempotent.
|
|
353
|
+
*/
|
|
354
|
+
public async pause(): Promise<void> {
|
|
355
|
+
if (!this.runningPromise?.isRunning()) {
|
|
356
|
+
this.log.debug('Sequencer not running, ignoring pause');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.log.info('Pausing sequencer');
|
|
360
|
+
// Halt the poll loop and let the in-flight iteration finish naturally — no interrupt, and no STOPPING
|
|
361
|
+
// state, since entering STOPPING would make the iteration's own setState calls throw and fail it.
|
|
362
|
+
// work() registers its fire-and-forget requests synchronously before returning, so once the loop has
|
|
363
|
+
// stopped pendingRequests holds every request that will ever exist; awaiting it lets them all finish.
|
|
364
|
+
await this.runningPromise.stop();
|
|
365
|
+
await this.pendingRequests.awaitRequests();
|
|
366
|
+
this.log.info('Paused sequencer');
|
|
367
|
+
}
|
|
368
|
+
|
|
305
369
|
/** Main sequencer loop with a try/catch */
|
|
306
370
|
protected async safeWork() {
|
|
307
371
|
try {
|
|
@@ -452,13 +516,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
452
516
|
return undefined;
|
|
453
517
|
}
|
|
454
518
|
|
|
455
|
-
// We are the proposer and the escape hatch is closed: now run the full sync check before building.
|
|
456
|
-
const syncedTo = await this.checkSync({ ts, slot });
|
|
457
|
-
if (!syncedTo) {
|
|
458
|
-
await this.tryVoteAndPruneWhenCannotBuild({ slot, targetSlot });
|
|
459
|
-
return undefined;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
519
|
// Explicit build-loop entry gate: if we are past the latest useful block-building start for the
|
|
463
520
|
// target slot, abandon building for this slot. The proposer prioritizes the ideal L1-publish path
|
|
464
521
|
// and does not plan around the late consensus-handoff path. This is the proposer build path's
|
|
@@ -480,6 +537,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
480
537
|
return undefined;
|
|
481
538
|
}
|
|
482
539
|
|
|
540
|
+
// We are the proposer, the escape hatch is closed, and we have time before the build start deadline.
|
|
541
|
+
// Now run the full sync check before building.
|
|
542
|
+
const syncedTo = await this.checkSync({ ts, slot });
|
|
543
|
+
if (!syncedTo) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
483
547
|
// Next checkpoint follows from the last synced one
|
|
484
548
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
485
549
|
|
|
@@ -707,6 +771,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
707
771
|
this.metrics,
|
|
708
772
|
this.checkpointProposalJobMetrics.createRecorder(),
|
|
709
773
|
this,
|
|
774
|
+
this.pendingRequests,
|
|
710
775
|
this.setState.bind(this),
|
|
711
776
|
this.tracer,
|
|
712
777
|
this.log.getBindings(),
|
|
@@ -1023,10 +1088,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1023
1088
|
// Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
|
|
1024
1089
|
// expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
|
|
1025
1090
|
// slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
|
|
1026
|
-
// work loop while waiting for the target slot to start.
|
|
1027
|
-
|
|
1091
|
+
// work loop while waiting for the target slot to start, but track it so stop() can drain it.
|
|
1092
|
+
const send = publisher.sendRequestsAt(targetSlot).catch(err => {
|
|
1028
1093
|
this.log.error(`Failed to publish fallback requests despite sync failure for slot ${slot}`, err, { slot });
|
|
1029
1094
|
});
|
|
1095
|
+
this.pendingRequests.trackRequest(send, () => publisher.interrupt());
|
|
1030
1096
|
}
|
|
1031
1097
|
|
|
1032
1098
|
private async tryEnqueuePruneIfPrunable(targetSlot: SlotNumber, publisher: SequencerPublisher): Promise<boolean> {
|
|
@@ -1096,10 +1162,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1096
1162
|
// the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
|
|
1097
1163
|
// `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
|
|
1098
1164
|
// silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
|
|
1099
|
-
// waiting for the target slot to start, mirroring tryVoteAndPruneWhenCannotBuild
|
|
1100
|
-
|
|
1165
|
+
// waiting for the target slot to start, mirroring tryVoteAndPruneWhenCannotBuild, but tracked so
|
|
1166
|
+
// stop() can drain it.
|
|
1167
|
+
const send = publisher.sendRequestsAt(targetSlot).catch(err => {
|
|
1101
1168
|
this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, { slot, targetSlot });
|
|
1102
1169
|
});
|
|
1170
|
+
this.pendingRequests.trackRequest(send, () => publisher.interrupt());
|
|
1103
1171
|
}
|
|
1104
1172
|
|
|
1105
1173
|
/**
|
package/src/test/utils.ts
CHANGED
|
@@ -90,6 +90,7 @@ export async function makeBlock(txs: Tx[], globalVariables: GlobalVariables): Pr
|
|
|
90
90
|
*/
|
|
91
91
|
export function mockPendingTxs(p2p: MockProxy<P2P>, txs: Tx[]): void {
|
|
92
92
|
p2p.getPendingTxCount.mockResolvedValue(txs.length);
|
|
93
|
+
p2p.hasEligiblePendingTxs.mockImplementation(minCount => Promise.resolve(txs.length >= minCount));
|
|
93
94
|
p2p.iteratePendingTxs.mockImplementation(() => mockTxIterator(Promise.resolve(txs)));
|
|
94
95
|
p2p.iterateEligiblePendingTxs.mockImplementation(() => mockTxIterator(Promise.resolve(txs)));
|
|
95
96
|
}
|