@aztec/sequencer-client 5.0.0-rc.1 → 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 +8 -3
- 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/global_variable_builder/global_builder.d.ts +3 -16
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +2 -25
- package/dest/publisher/config.d.ts +5 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +11 -1
- 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 +17 -4
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +119 -22
- 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 +8 -3
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +27 -14
- 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 +28 -26
- 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 +47 -10
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +125 -60
- package/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +2 -1
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +19 -3
- package/src/config.ts +8 -3
- package/src/global_variable_builder/fee_predictor.ts +11 -1
- package/src/global_variable_builder/fee_provider.ts +19 -2
- package/src/global_variable_builder/global_builder.ts +2 -34
- package/src/publisher/config.ts +22 -1
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/sequencer-publisher.ts +116 -23
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/automine_sequencer.ts +27 -14
- package/src/sequencer/checkpoint_proposal_job.ts +43 -34
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +133 -61
- package/src/test/utils.ts +2 -0
- package/dest/sequencer/chain_state_overrides.d.ts +0 -61
- package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
- package/dest/sequencer/chain_state_overrides.js +0 -98
- package/src/sequencer/chain_state_overrides.ts +0 -169
|
@@ -31,12 +31,14 @@ 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';
|
|
37
38
|
import {
|
|
38
39
|
type Checkpoint,
|
|
39
40
|
type ProposedCheckpointData,
|
|
41
|
+
buildCheckpointSimulationOverridesPlan,
|
|
40
42
|
getPreviousCheckpointOutHashes,
|
|
41
43
|
validateCheckpoint,
|
|
42
44
|
} from '@aztec/stdlib/checkpoint';
|
|
@@ -68,12 +70,12 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
68
70
|
|
|
69
71
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
70
72
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
71
|
-
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
72
73
|
import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
|
|
73
74
|
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,
|
|
@@ -398,9 +397,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
398
397
|
}
|
|
399
398
|
}
|
|
400
399
|
|
|
401
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
402
|
-
txTimeoutAt,
|
|
403
|
-
});
|
|
400
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, { txTimeoutAt });
|
|
404
401
|
}
|
|
405
402
|
|
|
406
403
|
/**
|
|
@@ -1051,7 +1048,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1051
1048
|
|
|
1052
1049
|
try {
|
|
1053
1050
|
// Wait until we have enough txs to build the block
|
|
1054
|
-
const {
|
|
1051
|
+
const { canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
1055
1052
|
if (!canStartBuilding) {
|
|
1056
1053
|
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1057
1054
|
reason: 'insufficient_txs',
|
|
@@ -1059,22 +1056,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1059
1056
|
slot: this.targetSlot,
|
|
1060
1057
|
checkpointNumber: this.checkpointNumber,
|
|
1061
1058
|
indexWithinCheckpoint,
|
|
1062
|
-
availableTxs,
|
|
1063
1059
|
minTxs,
|
|
1064
1060
|
});
|
|
1065
|
-
this.log.
|
|
1066
|
-
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (
|
|
1061
|
+
this.log.verbose(
|
|
1062
|
+
`Not enough age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (needs ${minTxs} eligible)`,
|
|
1067
1063
|
{
|
|
1068
1064
|
reason: 'insufficient_txs',
|
|
1069
1065
|
blockNumber,
|
|
1070
1066
|
slot: this.targetSlot,
|
|
1071
1067
|
checkpointNumber: this.checkpointNumber,
|
|
1072
1068
|
indexWithinCheckpoint,
|
|
1073
|
-
availableTxs,
|
|
1074
1069
|
minTxs,
|
|
1075
1070
|
},
|
|
1076
1071
|
);
|
|
1077
|
-
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs,
|
|
1072
|
+
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, slot: this.targetSlot });
|
|
1078
1073
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
1079
1074
|
return { failure: 'insufficient-txs' };
|
|
1080
1075
|
}
|
|
@@ -1088,10 +1083,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1088
1083
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
1089
1084
|
);
|
|
1090
1085
|
|
|
1091
|
-
this.log.debug(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1086
|
+
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot}`, {
|
|
1087
|
+
slot: this.targetSlot,
|
|
1088
|
+
blockNumber,
|
|
1089
|
+
indexWithinCheckpoint,
|
|
1090
|
+
});
|
|
1095
1091
|
this.setState(SequencerState.CREATING_BLOCK);
|
|
1096
1092
|
|
|
1097
1093
|
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
@@ -1243,7 +1239,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1243
1239
|
blockNumber: BlockNumber;
|
|
1244
1240
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
1245
1241
|
buildDeadline: Date | undefined;
|
|
1246
|
-
}): Promise<{ canStartBuilding: boolean;
|
|
1242
|
+
}): Promise<{ canStartBuilding: boolean; minTxs: number }> {
|
|
1247
1243
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
1248
1244
|
|
|
1249
1245
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -1254,26 +1250,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1254
1250
|
? new Date(buildDeadline.getTime() - this.timetable.minBlockDuration * 1000)
|
|
1255
1251
|
: undefined;
|
|
1256
1252
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
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))) {
|
|
1260
1256
|
// If we're past deadline, or we have no deadline, give up
|
|
1261
1257
|
const now = this.dateProvider.nowAsDate();
|
|
1262
1258
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
1263
|
-
return { canStartBuilding: false,
|
|
1259
|
+
return { canStartBuilding: false, minTxs };
|
|
1264
1260
|
}
|
|
1265
1261
|
|
|
1266
1262
|
// Wait a bit before checking again
|
|
1267
1263
|
this.setState(SequencerState.WAITING_FOR_TXS);
|
|
1268
1264
|
this.log.verbose(
|
|
1269
|
-
`Waiting for
|
|
1270
|
-
{ 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 },
|
|
1271
1267
|
);
|
|
1272
1268
|
await this.waitForTxsPollingInterval();
|
|
1273
|
-
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
1274
1269
|
}
|
|
1275
1270
|
|
|
1276
|
-
return { canStartBuilding: true,
|
|
1271
|
+
return { canStartBuilding: true, minTxs };
|
|
1277
1272
|
}
|
|
1278
1273
|
|
|
1279
1274
|
private async getSignedCommitteeAttestations(
|
|
@@ -1389,6 +1384,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1389
1384
|
this.config.injectFakeAttestation ||
|
|
1390
1385
|
this.config.injectHighSValueAttestation ||
|
|
1391
1386
|
this.config.injectUnrecoverableSignatureAttestation ||
|
|
1387
|
+
this.config.injectYParityAttestation ||
|
|
1392
1388
|
this.config.shuffleAttestationOrdering
|
|
1393
1389
|
) {
|
|
1394
1390
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
@@ -1490,6 +1486,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1490
1486
|
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1491
1487
|
}
|
|
1492
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
|
+
|
|
1493
1502
|
if (this.config.shuffleAttestationOrdering) {
|
|
1494
1503
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
1495
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
|
+
}
|
|
@@ -18,7 +18,11 @@ import type {
|
|
|
18
18
|
ProposedCheckpointSink,
|
|
19
19
|
ValidateCheckpointResult,
|
|
20
20
|
} from '@aztec/stdlib/block';
|
|
21
|
-
import
|
|
21
|
+
import {
|
|
22
|
+
type Checkpoint,
|
|
23
|
+
type ProposedCheckpointData,
|
|
24
|
+
buildCheckpointSimulationOverridesPlan,
|
|
25
|
+
} from '@aztec/stdlib/checkpoint';
|
|
22
26
|
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
23
27
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
28
|
import {
|
|
@@ -45,13 +49,13 @@ import { DefaultSequencerConfig } from '../config.js';
|
|
|
45
49
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
46
50
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
47
51
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
48
|
-
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
49
52
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
50
53
|
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
51
54
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
52
55
|
import { SequencerInterruptedError } from './errors.js';
|
|
53
56
|
import type { SequencerEvents } from './events.js';
|
|
54
57
|
import { SequencerMetrics } from './metrics.js';
|
|
58
|
+
import { RequestsTracker } from './requests_tracker.js';
|
|
55
59
|
import type { SequencerRollupConstants } from './types.js';
|
|
56
60
|
import { SequencerState } from './utils.js';
|
|
57
61
|
|
|
@@ -76,7 +80,7 @@ type SequencerSlotContext = {
|
|
|
76
80
|
* - Votes for proposals and slashes on L1
|
|
77
81
|
*/
|
|
78
82
|
export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
|
|
79
|
-
|
|
83
|
+
protected runningPromise?: RunningPromise;
|
|
80
84
|
private state = SequencerState.STOPPED;
|
|
81
85
|
private stateSlotNumber: SlotNumber | undefined;
|
|
82
86
|
/** Wall-clock time (ms, via the date provider) at which the current state was entered. */
|
|
@@ -85,8 +89,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
85
89
|
private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
|
|
86
90
|
private readonly stateLog: Logger;
|
|
87
91
|
|
|
88
|
-
/** The last slot for which we attempted to perform our
|
|
89
|
-
private
|
|
92
|
+
/** The last slot for which we attempted to perform our fallback duties (votes and/or prune) with degraded block production */
|
|
93
|
+
private lastSlotForFallbackAction: SlotNumber | undefined;
|
|
90
94
|
|
|
91
95
|
/** The (checkpoint, slot) of the last invalidation request we successfully simulated, to prevent
|
|
92
96
|
* re-simulating and re-submitting the same invalidation across the many ticks within a single slot. */
|
|
@@ -107,6 +111,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
107
111
|
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
108
112
|
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
109
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
|
+
|
|
110
125
|
/** Proposer schedule and block sub-slot timetable for the sequencer, rebuilt on every config update. */
|
|
111
126
|
protected timetable!: ProposerTimetable;
|
|
112
127
|
|
|
@@ -270,8 +285,22 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
270
285
|
getKzg();
|
|
271
286
|
}
|
|
272
287
|
|
|
273
|
-
/**
|
|
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
|
+
*/
|
|
274
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
|
+
}
|
|
275
304
|
this.runningPromise = new RunningPromise(
|
|
276
305
|
this.safeWork.bind(this),
|
|
277
306
|
this.log,
|
|
@@ -287,18 +316,56 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
287
316
|
return this.runningPromise?.trigger();
|
|
288
317
|
}
|
|
289
318
|
|
|
290
|
-
/**
|
|
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
|
+
*/
|
|
291
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
|
+
}
|
|
292
331
|
this.log.info(`Stopping sequencer`);
|
|
293
332
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
294
333
|
this.lastCheckpointProposalJob?.interrupt();
|
|
295
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.
|
|
296
339
|
await this.runningPromise?.stop();
|
|
297
|
-
|
|
340
|
+
this.pendingRequests.interruptRequests();
|
|
341
|
+
await this.pendingRequests.awaitRequests();
|
|
298
342
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
299
343
|
this.log.info('Stopped sequencer');
|
|
300
344
|
}
|
|
301
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
|
+
|
|
302
369
|
/** Main sequencer loop with a try/catch */
|
|
303
370
|
protected async safeWork() {
|
|
304
371
|
try {
|
|
@@ -449,13 +516,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
449
516
|
return undefined;
|
|
450
517
|
}
|
|
451
518
|
|
|
452
|
-
// We are the proposer and the escape hatch is closed: now run the full sync check before building.
|
|
453
|
-
const syncedTo = await this.checkSync({ ts, slot });
|
|
454
|
-
if (!syncedTo) {
|
|
455
|
-
await this.tryVoteWhenCannotBuild({ slot, targetSlot });
|
|
456
|
-
return undefined;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
519
|
// Explicit build-loop entry gate: if we are past the latest useful block-building start for the
|
|
460
520
|
// target slot, abandon building for this slot. The proposer prioritizes the ideal L1-publish path
|
|
461
521
|
// and does not plan around the late consensus-handoff path. This is the proposer build path's
|
|
@@ -472,11 +532,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
472
532
|
});
|
|
473
533
|
// Mark the slot as attempted so a deadline abort is not retried within the same slot. Vote-only actions
|
|
474
534
|
// still need to run because sync can succeed even when it is too late to start building a checkpoint.
|
|
475
|
-
await this.
|
|
535
|
+
await this.tryVoteAndPruneWhenCannotBuild({ slot, targetSlot });
|
|
476
536
|
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
477
537
|
return undefined;
|
|
478
538
|
}
|
|
479
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
|
+
|
|
480
547
|
// Next checkpoint follows from the last synced one
|
|
481
548
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
482
549
|
|
|
@@ -704,6 +771,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
704
771
|
this.metrics,
|
|
705
772
|
this.checkpointProposalJobMetrics.createRecorder(),
|
|
706
773
|
this,
|
|
774
|
+
this.pendingRequests,
|
|
707
775
|
this.setState.bind(this),
|
|
708
776
|
this.tracer,
|
|
709
777
|
this.log.getBindings(),
|
|
@@ -798,9 +866,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
798
866
|
number: syncSummary.latestBlockNumber,
|
|
799
867
|
hash: syncSummary.latestBlockHash,
|
|
800
868
|
})),
|
|
801
|
-
this.l2BlockSource
|
|
802
|
-
.getL2Tips()
|
|
803
|
-
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
869
|
+
this.l2BlockSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
804
870
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
805
871
|
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
806
872
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
@@ -845,16 +911,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
845
911
|
// matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
|
|
846
912
|
// would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
|
|
847
913
|
// once their build slot ends; this guard is the correctness barrier during the grace window before.
|
|
914
|
+
// `getProposedCheckpointData()` returns the latest proposed checkpoint payload, which is always
|
|
915
|
+
// the leading one (a proposed entry is only stored beyond the confirmed frontier and is deleted
|
|
916
|
+
// on confirmation). It carries no tip, so there is no tip-vs-payload split read to reconcile.
|
|
848
917
|
if (
|
|
849
918
|
blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number &&
|
|
850
|
-
|
|
851
|
-
proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)
|
|
919
|
+
proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber
|
|
852
920
|
) {
|
|
853
921
|
const logCtx = {
|
|
854
922
|
blockCheckpointNumber: blockData.checkpointNumber,
|
|
855
923
|
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
856
|
-
proposedCheckpointTipNumber:
|
|
857
|
-
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
924
|
+
proposedCheckpointTipNumber: proposedCheckpointData?.checkpointNumber,
|
|
858
925
|
blockNumber: blockData.header.getBlockNumber(),
|
|
859
926
|
blockSlot: blockData.header.getSlot(),
|
|
860
927
|
syncedL2Slot,
|
|
@@ -865,27 +932,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
865
932
|
return undefined;
|
|
866
933
|
}
|
|
867
934
|
|
|
868
|
-
const hasProposedCheckpoint =
|
|
869
|
-
|
|
870
|
-
// The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
|
|
871
|
-
// (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
|
|
872
|
-
// write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
|
|
873
|
-
// no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
|
|
874
|
-
// window — the next checkSync tick will see a coherent snapshot.
|
|
875
|
-
if (
|
|
876
|
-
hasProposedCheckpoint &&
|
|
877
|
-
(!proposedCheckpointData ||
|
|
878
|
-
proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)
|
|
879
|
-
) {
|
|
880
|
-
this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
|
|
881
|
-
proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
|
|
882
|
-
checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
|
|
883
|
-
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
884
|
-
syncedL2Slot,
|
|
885
|
-
...args,
|
|
886
|
-
});
|
|
887
|
-
return undefined;
|
|
888
|
-
}
|
|
935
|
+
const hasProposedCheckpoint = proposedCheckpointData !== undefined;
|
|
889
936
|
|
|
890
937
|
// Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
|
|
891
938
|
// The checkpoint number to build is derived as blockData.checkpointNumber + 1
|
|
@@ -962,16 +1009,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
962
1009
|
}
|
|
963
1010
|
|
|
964
1011
|
/**
|
|
965
|
-
* Tries to vote on slashing actions and governance when we cannot build and are past the
|
|
966
|
-
* This allows the sequencer to participate in governance/slashing votes even when it
|
|
1012
|
+
* Tries to vote on slashing actions and governance and to prune when we cannot build and are past the
|
|
1013
|
+
* block-building window. This allows the sequencer to participate in governance/slashing votes even when it
|
|
1014
|
+
* cannot build blocks, and to prune the pending chain so it can recover from bad data that is blocking sync.
|
|
967
1015
|
*/
|
|
968
|
-
@trackSpan('Sequencer.
|
|
969
|
-
protected async
|
|
1016
|
+
@trackSpan('Sequencer.tryVoteAndPruneWhenCannotBuild', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
1017
|
+
protected async tryVoteAndPruneWhenCannotBuild(args: { slot: SlotNumber; targetSlot: SlotNumber }): Promise<void> {
|
|
970
1018
|
const { slot, targetSlot } = args;
|
|
971
1019
|
|
|
972
1020
|
// Prevent duplicate attempts in the same slot
|
|
973
|
-
if (this.
|
|
974
|
-
this.log.trace(`Already attempted
|
|
1021
|
+
if (this.lastSlotForFallbackAction === slot) {
|
|
1022
|
+
this.log.trace(`Already attempted fallback actions in slot ${slot} (skipping)`);
|
|
975
1023
|
return;
|
|
976
1024
|
}
|
|
977
1025
|
|
|
@@ -994,7 +1042,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
994
1042
|
}
|
|
995
1043
|
|
|
996
1044
|
// Mark this slot as attempted
|
|
997
|
-
this.
|
|
1045
|
+
this.lastSlotForFallbackAction = slot;
|
|
998
1046
|
|
|
999
1047
|
// Get a publisher for voting
|
|
1000
1048
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
@@ -1019,19 +1067,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1019
1067
|
const votesPromises = voter.enqueueVotes();
|
|
1020
1068
|
const votes = await Promise.all(votesPromises);
|
|
1021
1069
|
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1070
|
+
// Even if we cannot build, try to prune so a stuck pending chain (e.g. bad data blocking sync) can
|
|
1071
|
+
// recover. prune() is permissionless, so it rides the same fallback multicall as the votes.
|
|
1072
|
+
const pruneEnqueued = await this.tryEnqueuePruneIfPrunable(targetSlot, publisher);
|
|
1073
|
+
|
|
1074
|
+
// Bail if nothing to do
|
|
1075
|
+
if (votes.every(p => !p) && !pruneEnqueued) {
|
|
1076
|
+
this.log.debug(`Nothing to enqueue for slot ${slot} (no votes, not prunable)`);
|
|
1024
1077
|
return;
|
|
1025
1078
|
}
|
|
1026
1079
|
|
|
1027
|
-
|
|
1080
|
+
const [governanceVoteEnqueued, slashingVoteEnqueued] = votes;
|
|
1081
|
+
this.log.info(`Submitting fallback requests in slot ${slot} despite sync failure`, {
|
|
1082
|
+
slot,
|
|
1083
|
+
pruneEnqueued,
|
|
1084
|
+
governanceVoteEnqueued: !!governanceVoteEnqueued,
|
|
1085
|
+
slashingVoteEnqueued: !!slashingVoteEnqueued,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1028
1088
|
// Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
|
|
1029
1089
|
// expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
|
|
1030
1090
|
// slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
|
|
1031
|
-
// work loop while waiting for the target slot to start.
|
|
1032
|
-
|
|
1033
|
-
this.log.error(`Failed to publish
|
|
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 => {
|
|
1093
|
+
this.log.error(`Failed to publish fallback requests despite sync failure for slot ${slot}`, err, { slot });
|
|
1034
1094
|
});
|
|
1095
|
+
this.pendingRequests.trackRequest(send, () => publisher.interrupt());
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private async tryEnqueuePruneIfPrunable(targetSlot: SlotNumber, publisher: SequencerPublisher): Promise<boolean> {
|
|
1099
|
+
try {
|
|
1100
|
+
return await publisher.enqueuePruneIfPrunable(targetSlot);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
this.log.error(`Failed to enqueue rollup prune for slot ${targetSlot}`, err, { targetSlot });
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1035
1105
|
}
|
|
1036
1106
|
|
|
1037
1107
|
/**
|
|
@@ -1047,13 +1117,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1047
1117
|
const { slot, targetSlot, proposer } = args;
|
|
1048
1118
|
|
|
1049
1119
|
// Prevent duplicate attempts in the same slot
|
|
1050
|
-
if (this.
|
|
1120
|
+
if (this.lastSlotForFallbackAction === slot) {
|
|
1051
1121
|
this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
|
|
1052
1122
|
return;
|
|
1053
1123
|
}
|
|
1054
1124
|
|
|
1055
1125
|
// Mark this slot as attempted
|
|
1056
|
-
this.
|
|
1126
|
+
this.lastSlotForFallbackAction = slot;
|
|
1057
1127
|
|
|
1058
1128
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
1059
1129
|
|
|
@@ -1092,10 +1162,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1092
1162
|
// the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
|
|
1093
1163
|
// `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
|
|
1094
1164
|
// silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
|
|
1095
|
-
// waiting for the target slot to start, mirroring
|
|
1096
|
-
|
|
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 => {
|
|
1097
1168
|
this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, { slot, targetSlot });
|
|
1098
1169
|
});
|
|
1170
|
+
this.pendingRequests.trackRequest(send, () => publisher.interrupt());
|
|
1099
1171
|
}
|
|
1100
1172
|
|
|
1101
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
|
}
|
|
@@ -129,6 +130,7 @@ function createCheckpointHeaderFromBlock(block: L2Block): CheckpointHeader {
|
|
|
129
130
|
gv.feeRecipient,
|
|
130
131
|
gv.gasFees,
|
|
131
132
|
block.header.totalManaUsed,
|
|
133
|
+
block.header.totalFees,
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|