@aztec/sequencer-client 0.0.1-commit.2f68f620 → 0.0.1-commit.321f6a9
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/README.md +27 -27
- 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 -12
- package/dest/config.d.ts +6 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -14
- 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 +27 -5
- 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/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- 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/l1_tx_failed_store/failed_tx_store.d.ts +48 -8
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +68 -1
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +2 -2
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +4 -2
- package/dest/publisher/l1_tx_failed_store/index.d.ts +2 -2
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/index.js +1 -0
- package/dest/publisher/sequencer-publisher.d.ts +34 -6
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +362 -61
- 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_factory.d.ts +5 -3
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_factory.js +2 -1
- package/dest/sequencer/automine/automine_sequencer.d.ts +43 -5
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +199 -18
- package/dest/sequencer/automine/index.d.ts +3 -0
- package/dest/sequencer/automine/index.d.ts.map +1 -0
- package/dest/sequencer/automine/index.js +2 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -16
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +98 -90
- package/dest/sequencer/events.d.ts +16 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -3
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -2
- 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 +110 -36
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +329 -174
- package/dest/test/index.d.ts +3 -3
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/utils.d.ts +15 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +18 -1
- package/package.json +30 -28
- package/src/client/sequencer-client.ts +20 -13
- package/src/config.ts +25 -12
- package/src/global_variable_builder/fee_predictor.ts +11 -1
- package/src/global_variable_builder/fee_provider.ts +27 -5
- package/src/global_variable_builder/global_builder.ts +2 -34
- package/src/index.ts +1 -10
- package/src/publisher/config.ts +22 -1
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +114 -7
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +4 -3
- package/src/publisher/l1_tx_failed_store/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +383 -72
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/README.md +18 -4
- package/src/sequencer/automine/automine_factory.ts +8 -1
- package/src/sequencer/automine/automine_sequencer.ts +221 -20
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +119 -100
- package/src/sequencer/events.ts +16 -4
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +360 -187
- package/src/test/index.ts +2 -2
- package/src/test/utils.ts +33 -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/dest/sequencer/timetable.d.ts +0 -98
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -227
- package/src/sequencer/chain_state_overrides.ts +0 -162
- package/src/sequencer/timetable.ts +0 -288
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EpochCache, PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
|
|
2
2
|
import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
|
|
3
3
|
import {
|
|
4
4
|
BlockNumber,
|
|
@@ -31,16 +31,18 @@ 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';
|
|
43
|
-
import { computeQuorum,
|
|
45
|
+
import { computeQuorum, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
44
46
|
import { Gas } from '@aztec/stdlib/gas';
|
|
45
47
|
import {
|
|
46
48
|
type BlockBuilderOptions,
|
|
@@ -59,6 +61,7 @@ import type {
|
|
|
59
61
|
} from '@aztec/stdlib/p2p';
|
|
60
62
|
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
61
63
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
64
|
+
import type { ProposerTimetable } from '@aztec/stdlib/timetable';
|
|
62
65
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
63
66
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
64
67
|
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
@@ -67,13 +70,12 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
67
70
|
|
|
68
71
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
69
72
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
70
|
-
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
71
73
|
import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
|
|
72
74
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
73
75
|
import { SequencerInterruptedError } from './errors.js';
|
|
74
76
|
import type { SequencerEvents } from './events.js';
|
|
75
77
|
import type { SequencerMetrics } from './metrics.js';
|
|
76
|
-
import type {
|
|
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
|
|
|
@@ -123,7 +123,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
constructor(
|
|
126
|
-
private readonly slotNow: SlotNumber,
|
|
127
126
|
private readonly targetSlot: SlotNumber,
|
|
128
127
|
private readonly targetEpoch: EpochNumber,
|
|
129
128
|
private readonly checkpointNumber: CheckpointNumber,
|
|
@@ -145,32 +144,46 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
145
144
|
private readonly l1Constants: SequencerRollupConstants,
|
|
146
145
|
private readonly signatureContext: CoordinationSignatureContext,
|
|
147
146
|
protected config: ResolvedSequencerConfig,
|
|
148
|
-
protected timetable:
|
|
147
|
+
protected timetable: ProposerTimetable,
|
|
149
148
|
private readonly slasherClient: SlasherClientInterface | undefined,
|
|
150
149
|
private readonly epochCache: EpochCache,
|
|
151
150
|
private readonly dateProvider: DateProvider,
|
|
152
151
|
private readonly metrics: SequencerMetrics,
|
|
153
152
|
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
154
153
|
protected readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
155
|
-
|
|
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,
|
|
157
|
+
private readonly setStateFn: (state: SequencerState, slot: SlotNumber) => void,
|
|
156
158
|
public readonly tracer: Tracer,
|
|
157
159
|
bindings?: LoggerBindings,
|
|
158
160
|
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
159
161
|
) {
|
|
160
162
|
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
161
163
|
...bindings,
|
|
162
|
-
instanceId: `slot-${this.
|
|
164
|
+
instanceId: `slot-${this.getBuildSlot()}`,
|
|
163
165
|
});
|
|
164
166
|
this.checkpointEventLog = createLogger('sequencer:checkpoint-events', {
|
|
165
167
|
...bindings,
|
|
166
|
-
instanceId: `slot-${this.
|
|
168
|
+
instanceId: `slot-${this.getBuildSlot()}`,
|
|
167
169
|
});
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
/**
|
|
173
|
+
* The wall-clock slot during which this job builds, i.e. the slot one before {@link targetSlot} under
|
|
174
|
+
* proposer pipelining. Also the slot of the parent checkpoint this job builds on top of.
|
|
175
|
+
*/
|
|
176
|
+
private getBuildSlot(): SlotNumber {
|
|
177
|
+
return SlotNumber(this.targetSlot - PROPOSER_PIPELINING_SLOT_OFFSET);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sets the sequencer state for this job, reporting the target slot the checkpoint is being proposed for
|
|
182
|
+
* (not the wall-clock build slot). The slot is informational on the event payload/metrics; the job knows
|
|
183
|
+
* its own target slot, so callers only pass the state.
|
|
184
|
+
*/
|
|
185
|
+
private setState(state: SequencerState): void {
|
|
186
|
+
this.setStateFn(state, this.targetSlot);
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
/** Interrupts job-owned waits, including the publisher's send-at-slot sleep, so shutdown can finish. */
|
|
@@ -241,7 +254,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
241
254
|
// signature verification to fail silently inside Multicall3. Delay submission to the
|
|
242
255
|
// start of `targetSlot` so the tx mines in the slot the vote was signed for.
|
|
243
256
|
if (!this.config.fishermanMode) {
|
|
244
|
-
this.
|
|
257
|
+
this.pendingRequests.trackRequest(this.publisher.sendRequestsAt(this.targetSlot), () => this.interrupt());
|
|
245
258
|
}
|
|
246
259
|
return undefined;
|
|
247
260
|
}
|
|
@@ -256,7 +269,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
259
|
-
this.
|
|
272
|
+
this.pendingRequests.trackRequest(this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises), () =>
|
|
273
|
+
this.interrupt(),
|
|
274
|
+
);
|
|
260
275
|
|
|
261
276
|
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
262
277
|
return checkpoint;
|
|
@@ -264,7 +279,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
264
279
|
|
|
265
280
|
/**
|
|
266
281
|
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
267
|
-
* 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.
|
|
268
283
|
*/
|
|
269
284
|
private async waitForAttestationsAndEnqueueSubmissionAsync(
|
|
270
285
|
broadcast: CheckpointProposalBroadcast,
|
|
@@ -286,7 +301,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
286
301
|
}
|
|
287
302
|
|
|
288
303
|
// If we failed to collect attestations, at least check if we need to issue an invalidation
|
|
289
|
-
if (!signedAttestations && (await this.waitForSyncedL2SlotNumber(this.
|
|
304
|
+
if (!signedAttestations && (await this.waitForSyncedL2SlotNumber(this.getBuildSlot()))) {
|
|
290
305
|
const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
|
|
291
306
|
if (!validationStatus.valid) {
|
|
292
307
|
this.log.warn(
|
|
@@ -355,11 +370,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
355
370
|
private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
|
|
356
371
|
const { checkpoint, attestations, attestationsSignature } = result;
|
|
357
372
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
this.setState(SequencerState.PUBLISHING_CHECKPOINT);
|
|
374
|
+
// Latest L1 block the propose can still land in for the target slot: the last Ethereum block inside
|
|
375
|
+
// the target slot (`target_slot_start + S - E`). This is one ethereum slot later than
|
|
376
|
+
// `attestation_deadline` (= last_ethereum_block_in_target_slot - E), which bounds when validators must
|
|
377
|
+
// have signed, not when the proposer must have sent. Using the attestation deadline here is too tight:
|
|
378
|
+
// attestations are collected up to (and, when not enforcing, past) it, so the propose tx would be
|
|
379
|
+
// enqueued already expired and time out before it can mine.
|
|
380
|
+
const lastL1BlockInTargetSlot =
|
|
381
|
+
Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) +
|
|
382
|
+
this.l1Constants.slotDuration -
|
|
383
|
+
this.l1Constants.ethereumSlotDuration;
|
|
384
|
+
const txTimeoutAt = new Date(lastL1BlockInTargetSlot * 1000);
|
|
363
385
|
|
|
364
386
|
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
365
387
|
if (
|
|
@@ -375,9 +397,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
375
397
|
}
|
|
376
398
|
}
|
|
377
399
|
|
|
378
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
379
|
-
txTimeoutAt,
|
|
380
|
-
});
|
|
400
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, { txTimeoutAt });
|
|
381
401
|
}
|
|
382
402
|
|
|
383
403
|
/**
|
|
@@ -433,8 +453,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
433
453
|
|
|
434
454
|
const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
|
|
435
455
|
|
|
436
|
-
// Wait until archiver has synced L1 past the parent's slot (
|
|
437
|
-
if (!(await this.waitForSyncedL2SlotNumber(this.
|
|
456
|
+
// Wait until archiver has synced L1 past the parent's slot (the build slot, one before targetSlot)
|
|
457
|
+
if (!(await this.waitForSyncedL2SlotNumber(this.getBuildSlot()))) {
|
|
438
458
|
return false;
|
|
439
459
|
}
|
|
440
460
|
|
|
@@ -541,7 +561,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
541
561
|
// Measure against the wall-clock slot whose build window we are currently using.
|
|
542
562
|
// In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
|
|
543
563
|
// target-slot boundary a full slot away from the actual build start time.
|
|
544
|
-
const slotBoundaryMs = Number(getTimestampForSlot(this.
|
|
564
|
+
const slotBoundaryMs = Number(getTimestampForSlot(this.getBuildSlot(), this.l1Constants)) * 1000;
|
|
545
565
|
this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
|
|
546
566
|
}
|
|
547
567
|
this.checkpointMetrics.startCheckpointTiming(now);
|
|
@@ -551,9 +571,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
551
571
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
552
572
|
|
|
553
573
|
// Start the checkpoint
|
|
554
|
-
this.
|
|
574
|
+
this.setState(SequencerState.INITIALIZING_CHECKPOINT);
|
|
555
575
|
this.logCheckpointEvent('slot-started', `Starting checkpoint proposal for slot ${this.targetSlot}`, {
|
|
556
|
-
buildSlot: this.
|
|
576
|
+
buildSlot: this.getBuildSlot(),
|
|
557
577
|
submissionSlot: this.targetSlot,
|
|
558
578
|
slot: this.targetSlot,
|
|
559
579
|
checkpointNumber: this.checkpointNumber,
|
|
@@ -704,7 +724,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
704
724
|
|
|
705
725
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
706
726
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
707
|
-
this.
|
|
727
|
+
this.setState(SequencerState.ASSEMBLING_CHECKPOINT);
|
|
708
728
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
709
729
|
|
|
710
730
|
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
@@ -725,7 +745,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
725
745
|
reason: 'invalid_checkpoint',
|
|
726
746
|
checkpoint: checkpoint.header.toInspect(),
|
|
727
747
|
});
|
|
728
|
-
this.log.error(`Built an invalid checkpoint at slot ${this.
|
|
748
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.targetSlot} (skipping proposal)`, err, {
|
|
729
749
|
slot: this.targetSlot,
|
|
730
750
|
checkpointNumber: this.checkpointNumber,
|
|
731
751
|
blocksBuilt: blocksInCheckpoint.length,
|
|
@@ -744,7 +764,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
744
764
|
);
|
|
745
765
|
this.logCheckpointEvent('built', `Checkpoint built for slot ${this.targetSlot}`, {
|
|
746
766
|
slot: this.targetSlot,
|
|
747
|
-
buildSlot: this.
|
|
767
|
+
buildSlot: this.getBuildSlot(),
|
|
748
768
|
checkpointNumber: this.checkpointNumber,
|
|
749
769
|
proposer: this.proposer?.toString(),
|
|
750
770
|
attestorAddress: this.attestorAddress.toString(),
|
|
@@ -868,14 +888,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
868
888
|
break;
|
|
869
889
|
}
|
|
870
890
|
|
|
871
|
-
const
|
|
872
|
-
const timingInfo = this.timetable.
|
|
891
|
+
const nowSeconds = this.dateProvider.now() / 1000;
|
|
892
|
+
const timingInfo = this.timetable.selectNextSubslot(this.targetSlot, nowSeconds);
|
|
873
893
|
|
|
874
894
|
if (!timingInfo.canStart) {
|
|
875
895
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
876
896
|
slot: this.targetSlot,
|
|
877
897
|
blocksBuilt,
|
|
878
|
-
|
|
898
|
+
nowSeconds,
|
|
879
899
|
});
|
|
880
900
|
break;
|
|
881
901
|
}
|
|
@@ -885,10 +905,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
885
905
|
blockTimestamp: timestamp,
|
|
886
906
|
// Create an empty block if we haven't already and this is the last one
|
|
887
907
|
forceCreate: timingInfo.isLastBlock && blocksBuilt === 0 && this.config.buildCheckpointIfEmpty,
|
|
888
|
-
|
|
889
|
-
buildDeadline: timingInfo.deadline
|
|
890
|
-
? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000)
|
|
891
|
-
: undefined,
|
|
908
|
+
buildDeadline: new Date(timingInfo.deadline * 1000),
|
|
892
909
|
blockNumber,
|
|
893
910
|
indexWithinCheckpoint,
|
|
894
911
|
txHashesAlreadyIncluded,
|
|
@@ -896,8 +913,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
896
913
|
|
|
897
914
|
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
898
915
|
if ('failure' in buildResult) {
|
|
899
|
-
// If this was the last subslot,
|
|
900
|
-
if (timingInfo.isLastBlock
|
|
916
|
+
// If this was the last subslot, we're done.
|
|
917
|
+
if (timingInfo.isLastBlock) {
|
|
901
918
|
break;
|
|
902
919
|
}
|
|
903
920
|
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
@@ -993,14 +1010,17 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
993
1010
|
);
|
|
994
1011
|
}
|
|
995
1012
|
|
|
996
|
-
/**
|
|
1013
|
+
/**
|
|
1014
|
+
* Sleeps until it is time to produce the next block in the slot.
|
|
1015
|
+
* @param nextSubslotStart - Absolute wall-clock timestamp in seconds of the previous sub-slot deadline.
|
|
1016
|
+
*/
|
|
997
1017
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
998
|
-
|
|
999
|
-
this.
|
|
1000
|
-
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s
|
|
1018
|
+
protected async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
1019
|
+
this.setState(SequencerState.WAITING_UNTIL_NEXT_BLOCK);
|
|
1020
|
+
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s`, {
|
|
1001
1021
|
slot: this.targetSlot,
|
|
1002
1022
|
});
|
|
1003
|
-
await this.
|
|
1023
|
+
await this.awaitInterruptibleSleep(Math.max(0, nextSubslotStart * 1000 - this.dateProvider.now()));
|
|
1004
1024
|
}
|
|
1005
1025
|
|
|
1006
1026
|
/** Builds a single block. Called from the main block building loop. */
|
|
@@ -1028,7 +1048,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1028
1048
|
|
|
1029
1049
|
try {
|
|
1030
1050
|
// Wait until we have enough txs to build the block
|
|
1031
|
-
const {
|
|
1051
|
+
const { canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
1032
1052
|
if (!canStartBuilding) {
|
|
1033
1053
|
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1034
1054
|
reason: 'insufficient_txs',
|
|
@@ -1036,38 +1056,39 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1036
1056
|
slot: this.targetSlot,
|
|
1037
1057
|
checkpointNumber: this.checkpointNumber,
|
|
1038
1058
|
indexWithinCheckpoint,
|
|
1039
|
-
availableTxs,
|
|
1040
1059
|
minTxs,
|
|
1041
1060
|
});
|
|
1042
|
-
this.log.
|
|
1043
|
-
`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)`,
|
|
1044
1063
|
{
|
|
1045
1064
|
reason: 'insufficient_txs',
|
|
1046
1065
|
blockNumber,
|
|
1047
1066
|
slot: this.targetSlot,
|
|
1048
1067
|
checkpointNumber: this.checkpointNumber,
|
|
1049
1068
|
indexWithinCheckpoint,
|
|
1050
|
-
availableTxs,
|
|
1051
1069
|
minTxs,
|
|
1052
1070
|
},
|
|
1053
1071
|
);
|
|
1054
|
-
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs,
|
|
1072
|
+
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, slot: this.targetSlot });
|
|
1055
1073
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
1056
1074
|
return { failure: 'insufficient-txs' };
|
|
1057
1075
|
}
|
|
1058
1076
|
|
|
1059
1077
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
1060
1078
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
1079
|
+
// Block building only executes txs, so we skip loading their proofs unless these same tx objects get attached
|
|
1080
|
+
// to the broadcasted proposals via publishTxsWithProposals.
|
|
1061
1081
|
const pendingTxs = filter(
|
|
1062
|
-
this.p2pClient.iterateEligiblePendingTxs(),
|
|
1082
|
+
this.p2pClient.iterateEligiblePendingTxs({ includeProof: !!this.config.publishTxsWithProposals }),
|
|
1063
1083
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
1064
1084
|
);
|
|
1065
1085
|
|
|
1066
|
-
this.log.debug(
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1086
|
+
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot}`, {
|
|
1087
|
+
slot: this.targetSlot,
|
|
1088
|
+
blockNumber,
|
|
1089
|
+
indexWithinCheckpoint,
|
|
1090
|
+
});
|
|
1091
|
+
this.setState(SequencerState.CREATING_BLOCK);
|
|
1071
1092
|
|
|
1072
1093
|
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
1073
1094
|
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
@@ -1082,8 +1103,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1082
1103
|
deadline: buildDeadline,
|
|
1083
1104
|
isBuildingProposal: true,
|
|
1084
1105
|
minValidTxs,
|
|
1085
|
-
maxBlocksPerCheckpoint: this.timetable.
|
|
1106
|
+
maxBlocksPerCheckpoint: this.timetable.getMaxBlocksPerCheckpoint(),
|
|
1086
1107
|
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
1108
|
+
perBlockDAAllocationMultiplier: this.config.perBlockDAAllocationMultiplier,
|
|
1087
1109
|
};
|
|
1088
1110
|
|
|
1089
1111
|
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
@@ -1157,7 +1179,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1157
1179
|
checkpointNumber: this.checkpointNumber,
|
|
1158
1180
|
indexWithinCheckpoint: block.indexWithinCheckpoint,
|
|
1159
1181
|
slot: this.targetSlot,
|
|
1160
|
-
buildSlot: this.
|
|
1182
|
+
buildSlot: this.getBuildSlot(),
|
|
1161
1183
|
});
|
|
1162
1184
|
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
1163
1185
|
|
|
@@ -1217,44 +1239,43 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1217
1239
|
blockNumber: BlockNumber;
|
|
1218
1240
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
1219
1241
|
buildDeadline: Date | undefined;
|
|
1220
|
-
}): Promise<{ canStartBuilding: boolean;
|
|
1242
|
+
}): Promise<{ canStartBuilding: boolean; minTxs: number }> {
|
|
1221
1243
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
1222
1244
|
|
|
1223
1245
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
1224
1246
|
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
1225
1247
|
|
|
1226
|
-
//
|
|
1248
|
+
// Latest time to keep waiting for txs: wait_for_txs_deadline = block_build_deadline(k) - min_block_duration.
|
|
1227
1249
|
const startBuildingDeadline = buildDeadline
|
|
1228
|
-
? new Date(buildDeadline.getTime() - this.timetable.
|
|
1250
|
+
? new Date(buildDeadline.getTime() - this.timetable.minBlockDuration * 1000)
|
|
1229
1251
|
: undefined;
|
|
1230
1252
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
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))) {
|
|
1234
1256
|
// If we're past deadline, or we have no deadline, give up
|
|
1235
1257
|
const now = this.dateProvider.nowAsDate();
|
|
1236
1258
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
1237
|
-
return { canStartBuilding: false,
|
|
1259
|
+
return { canStartBuilding: false, minTxs };
|
|
1238
1260
|
}
|
|
1239
1261
|
|
|
1240
1262
|
// Wait a bit before checking again
|
|
1241
|
-
this.
|
|
1263
|
+
this.setState(SequencerState.WAITING_FOR_TXS);
|
|
1242
1264
|
this.log.verbose(
|
|
1243
|
-
`Waiting for
|
|
1244
|
-
{ 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 },
|
|
1245
1267
|
);
|
|
1246
1268
|
await this.waitForTxsPollingInterval();
|
|
1247
|
-
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
1248
1269
|
}
|
|
1249
1270
|
|
|
1250
|
-
return { canStartBuilding: true,
|
|
1271
|
+
return { canStartBuilding: true, minTxs };
|
|
1251
1272
|
}
|
|
1252
1273
|
|
|
1253
1274
|
private async getSignedCommitteeAttestations(
|
|
1254
1275
|
broadcast: CheckpointProposalBroadcast,
|
|
1255
1276
|
): Promise<{ attestations: CommitteeAttestationsAndSigners; attestationsSignature: Signature } | undefined> {
|
|
1256
1277
|
const { proposal, blockProposedAt } = broadcast;
|
|
1257
|
-
this.
|
|
1278
|
+
this.setState(SequencerState.COLLECTING_ATTESTATIONS);
|
|
1258
1279
|
const attestations = await this.waitForAttestations(proposal);
|
|
1259
1280
|
if (!attestations) {
|
|
1260
1281
|
return undefined;
|
|
@@ -1319,12 +1340,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1319
1340
|
);
|
|
1320
1341
|
}
|
|
1321
1342
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
1343
|
+
// Hard attestation-collection cutoff = the single consensus attestation_deadline (target_slot_start + S - 2E).
|
|
1344
|
+
const attestationDeadlineSeconds = this.timetable.getAttestationDeadline(this.targetSlot);
|
|
1345
|
+
const attestationDeadline = new Date(attestationDeadlineSeconds * 1000);
|
|
1326
1346
|
|
|
1327
|
-
this.metrics.recordRequiredAttestations(
|
|
1347
|
+
this.metrics.recordRequiredAttestations(
|
|
1348
|
+
numberOfRequiredAttestations,
|
|
1349
|
+
Math.max(0, attestationDeadline.getTime() - this.dateProvider.now()),
|
|
1350
|
+
);
|
|
1328
1351
|
|
|
1329
1352
|
const collectAttestationsTimer = new Timer();
|
|
1330
1353
|
let collectedAttestationsCount: number = 0;
|
|
@@ -1361,6 +1384,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1361
1384
|
this.config.injectFakeAttestation ||
|
|
1362
1385
|
this.config.injectHighSValueAttestation ||
|
|
1363
1386
|
this.config.injectUnrecoverableSignatureAttestation ||
|
|
1387
|
+
this.config.injectYParityAttestation ||
|
|
1364
1388
|
this.config.shuffleAttestationOrdering
|
|
1365
1389
|
) {
|
|
1366
1390
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
@@ -1462,6 +1486,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1462
1486
|
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1463
1487
|
}
|
|
1464
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
|
+
|
|
1465
1502
|
if (this.config.shuffleAttestationOrdering) {
|
|
1466
1503
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
1467
1504
|
|
|
@@ -1604,29 +1641,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1604
1641
|
return false;
|
|
1605
1642
|
}
|
|
1606
1643
|
|
|
1607
|
-
/** Waits
|
|
1608
|
-
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
1609
|
-
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
1610
|
-
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1611
|
-
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
1612
|
-
const delayMs = targetTimestamp * 1000 - this.dateProvider.nowAsDate().getTime();
|
|
1613
|
-
await this.awaitInterruptibleSleep(delayMs);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
/** Pause between mempool polls in waitForMinTxs. Extracted for test overriding. */
|
|
1644
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
1617
1645
|
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
1618
1646
|
await this.awaitInterruptibleSleep(TXS_POLLING_MS);
|
|
1619
1647
|
}
|
|
1620
1648
|
|
|
1621
|
-
private getSlotStartBuildTimestamp(): number {
|
|
1622
|
-
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
private getSecondsIntoSlot(): number {
|
|
1626
|
-
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1627
|
-
return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
1649
|
public getPublisher() {
|
|
1631
1650
|
return this.publisher;
|
|
1632
1651
|
}
|
package/src/sequencer/events.ts
CHANGED
|
@@ -5,12 +5,24 @@ import type { Action } from '../publisher/sequencer-publisher.js';
|
|
|
5
5
|
import type { SequencerState } from './utils.js';
|
|
6
6
|
|
|
7
7
|
export type SequencerEvents = {
|
|
8
|
+
/**
|
|
9
|
+
* Emitted on every sequencer state transition (including no-op transitions to the same state). The
|
|
10
|
+
* timing fields are anchored to the build frame of the slot being proposed for, not to wall-clock
|
|
11
|
+
* slot boundaries, because the proposer builds for `targetSlot` during the previous (build) slot.
|
|
12
|
+
*
|
|
13
|
+
* - `oldState` / `newState` are the previous and new {@link SequencerState}.
|
|
14
|
+
* - `secondsIntoBuildFrame` is the wall-clock seconds elapsed since the build-frame start of
|
|
15
|
+
* `targetSlot` (`now − getBuildFrameStart(targetSlot)`). Undefined for lifecycle states with no
|
|
16
|
+
* associated slot (e.g. IDLE/STOPPING). It can be negative if the transition happens before the
|
|
17
|
+
* build frame opens.
|
|
18
|
+
* - `targetSlot` is the slot the checkpoint is being proposed for (the submission slot, one ahead of
|
|
19
|
+
* the wall-clock build slot under pipelining). Undefined for lifecycle states with no slot.
|
|
20
|
+
*/
|
|
8
21
|
['state-changed']: (args: {
|
|
9
22
|
oldState: SequencerState;
|
|
10
23
|
newState: SequencerState;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
timeReferenceSlot?: SlotNumber;
|
|
24
|
+
secondsIntoBuildFrame?: number;
|
|
25
|
+
targetSlot?: SlotNumber;
|
|
14
26
|
}) => void;
|
|
15
27
|
/**
|
|
16
28
|
* Emitted by the sequencer once it has decided it is going to attempt to build a
|
|
@@ -37,7 +49,7 @@ export type SequencerEvents = {
|
|
|
37
49
|
simulatedPending: CheckpointNumber | undefined;
|
|
38
50
|
}) => void;
|
|
39
51
|
['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
40
|
-
['block-tx-count-check-failed']: (args: { minTxs: number;
|
|
52
|
+
['block-tx-count-check-failed']: (args: { minTxs: number; slot: SlotNumber }) => void;
|
|
41
53
|
['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
42
54
|
['block-proposed']: (args: {
|
|
43
55
|
blockNumber: BlockNumber;
|
package/src/sequencer/index.ts
CHANGED
|
@@ -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
|
+
}
|