@aztec/sequencer-client 0.0.1-commit.c2eed6949 → 0.0.1-commit.c52d6e7
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 +281 -21
- package/dest/client/sequencer-client.d.ts +20 -5
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +26 -22
- package/dest/config.d.ts +12 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +61 -26
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +138 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +80 -0
- package/dest/global_variable_builder/global_builder.d.ts +6 -25
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +7 -65
- package/dest/global_variable_builder/index.d.ts +3 -1
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/publisher/config.d.ts +7 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +13 -3
- 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 +50 -11
- 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-bundle-simulator.d.ts +96 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
- package/dest/publisher/sequencer-bundle-simulator.js +198 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +0 -1
- package/dest/publisher/sequencer-publisher.d.ts +101 -69
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +635 -538
- 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 +56 -0
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_factory.js +85 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_sequencer.js +696 -0
- 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 +71 -21
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +733 -213
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +61 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +9 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +34 -20
- package/dest/sequencer/missing_committee.d.ts +72 -0
- package/dest/sequencer/missing_committee.d.ts.map +1 -0
- package/dest/sequencer/missing_committee.js +139 -0
- 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 +158 -35
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +556 -162
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- 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 +25 -7
- package/package.json +29 -28
- package/src/client/sequencer-client.ts +37 -27
- package/src/config.ts +71 -26
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +182 -0
- package/src/global_variable_builder/fee_provider.ts +97 -0
- package/src/global_variable_builder/global_builder.ts +12 -80
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +30 -7
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +117 -8
- 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-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +735 -612
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/README.md +162 -450
- package/src/sequencer/automine/README.md +60 -0
- package/src/sequencer/automine/automine_factory.ts +152 -0
- package/src/sequencer/automine/automine_sequencer.ts +800 -0
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +849 -242
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +66 -5
- package/src/sequencer/metrics.ts +43 -24
- package/src/sequencer/missing_committee.ts +192 -0
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +639 -180
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -2
- package/src/test/utils.ts +61 -10
- package/dest/sequencer/timetable.d.ts +0 -88
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -222
- package/src/sequencer/timetable.ts +0 -283
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Attributes,
|
|
3
|
+
type Gauge,
|
|
4
|
+
type Histogram,
|
|
5
|
+
type Meter,
|
|
6
|
+
Metrics,
|
|
7
|
+
type TelemetryClient,
|
|
8
|
+
} from '@aztec/telemetry-client';
|
|
9
|
+
|
|
10
|
+
type CheckpointProposalJobInstruments = {
|
|
11
|
+
checkpointAttestationDelay: Histogram;
|
|
12
|
+
checkpointBuildDuration: Histogram;
|
|
13
|
+
checkpointStartToFirstBlockDuration: Histogram;
|
|
14
|
+
checkpointLastBlockToBroadcastDuration: Histogram;
|
|
15
|
+
pipelinedCheckpointBuildStartOffsetFromSlotBoundary: Histogram;
|
|
16
|
+
checkpointBlockCount: Gauge;
|
|
17
|
+
checkpointTxCount: Gauge;
|
|
18
|
+
checkpointTotalMana: Gauge;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-job recording surface used by {@link CheckpointProposalJob}.
|
|
23
|
+
*
|
|
24
|
+
* Ownership split:
|
|
25
|
+
* - {@link CheckpointProposalJobMetrics} owns the OpenTelemetry meter and instruments and should live for the
|
|
26
|
+
* lifetime of the sequencer process.
|
|
27
|
+
* - A recorder owns only mutable timing state for one checkpoint proposal job so overlapping jobs cannot
|
|
28
|
+
* overwrite each other's timing markers.
|
|
29
|
+
*/
|
|
30
|
+
export interface CheckpointProposalJobMetricsRecorder {
|
|
31
|
+
recordCheckpointAttestationDelay(durationMs: number): void;
|
|
32
|
+
recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number): void;
|
|
33
|
+
recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number): void;
|
|
34
|
+
startCheckpointTiming(nowMs: number): void;
|
|
35
|
+
noteCheckpointBlockBuilt(nowMs: number, opts: { isFirstBlock: boolean; isLastBlock: boolean }): void;
|
|
36
|
+
noteCheckpointBroadcast(nowMs: number): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Concrete per-job recorder.
|
|
41
|
+
*
|
|
42
|
+
* This class should be short-lived: create one recorder per checkpoint proposal job and discard it when the job
|
|
43
|
+
* completes. It intentionally does not create instruments; it only holds the job-local timestamps needed to derive
|
|
44
|
+
* timing metrics safely.
|
|
45
|
+
*/
|
|
46
|
+
class CheckpointProposalJobMetricsRecorderImpl implements CheckpointProposalJobMetricsRecorder {
|
|
47
|
+
private checkpointStartedAt?: number;
|
|
48
|
+
private checkpointLastBlockBuiltAt?: number;
|
|
49
|
+
|
|
50
|
+
constructor(private readonly instruments: CheckpointProposalJobInstruments) {}
|
|
51
|
+
|
|
52
|
+
public recordCheckpointAttestationDelay(durationMs: number) {
|
|
53
|
+
this.instruments.checkpointAttestationDelay.record(Math.ceil(durationMs));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
|
|
57
|
+
this.instruments.checkpointBuildDuration.record(Math.ceil(durationMs));
|
|
58
|
+
this.instruments.checkpointBlockCount.record(blockCount);
|
|
59
|
+
this.instruments.checkpointTxCount.record(txCount);
|
|
60
|
+
this.instruments.checkpointTotalMana.record(totalMana);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number) {
|
|
64
|
+
this.instruments.pipelinedCheckpointBuildStartOffsetFromSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
|
|
65
|
+
[Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public startCheckpointTiming(nowMs: number) {
|
|
70
|
+
this.checkpointStartedAt = nowMs;
|
|
71
|
+
this.checkpointLastBlockBuiltAt = undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public noteCheckpointBlockBuilt(nowMs: number, opts: { isFirstBlock: boolean; isLastBlock: boolean }) {
|
|
75
|
+
if (opts.isFirstBlock && this.checkpointStartedAt !== undefined) {
|
|
76
|
+
this.instruments.checkpointStartToFirstBlockDuration.record(Math.ceil(nowMs - this.checkpointStartedAt));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (opts.isLastBlock) {
|
|
80
|
+
this.checkpointLastBlockBuiltAt = nowMs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public noteCheckpointBroadcast(nowMs: number) {
|
|
85
|
+
if (this.checkpointLastBlockBuiltAt !== undefined) {
|
|
86
|
+
this.instruments.checkpointLastBlockToBroadcastDuration.record(
|
|
87
|
+
Math.ceil(nowMs - this.checkpointLastBlockBuiltAt),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.checkpointLastBlockBuiltAt = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Long-lived owner of checkpoint proposal job telemetry instruments.
|
|
97
|
+
*
|
|
98
|
+
* The sequencer should construct this once and reuse it across jobs. Each call to {@link createRecorder} returns a
|
|
99
|
+
* lightweight recorder with isolated mutable state for a single checkpoint proposal job.
|
|
100
|
+
*/
|
|
101
|
+
export class CheckpointProposalJobMetrics {
|
|
102
|
+
private readonly meter: Meter;
|
|
103
|
+
private readonly instruments: CheckpointProposalJobInstruments;
|
|
104
|
+
|
|
105
|
+
constructor(client: TelemetryClient, name = 'CheckpointProposalJob') {
|
|
106
|
+
this.meter = client.getMeter(name);
|
|
107
|
+
this.instruments = {
|
|
108
|
+
checkpointAttestationDelay: this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY),
|
|
109
|
+
checkpointBuildDuration: this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION),
|
|
110
|
+
checkpointStartToFirstBlockDuration: this.meter.createHistogram(
|
|
111
|
+
Metrics.SEQUENCER_CHECKPOINT_START_TO_FIRST_BLOCK_DURATION,
|
|
112
|
+
),
|
|
113
|
+
checkpointLastBlockToBroadcastDuration: this.meter.createHistogram(
|
|
114
|
+
Metrics.SEQUENCER_CHECKPOINT_LAST_BLOCK_TO_BROADCAST_DURATION,
|
|
115
|
+
),
|
|
116
|
+
pipelinedCheckpointBuildStartOffsetFromSlotBoundary: this.meter.createHistogram(
|
|
117
|
+
Metrics.SEQUENCER_PIPELINED_CHECKPOINT_BUILD_START_OFFSET_FROM_SLOT_BOUNDARY,
|
|
118
|
+
),
|
|
119
|
+
checkpointBlockCount: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT),
|
|
120
|
+
checkpointTxCount: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT),
|
|
121
|
+
checkpointTotalMana: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public createRecorder(): CheckpointProposalJobMetricsRecorder {
|
|
126
|
+
return new CheckpointProposalJobMetricsRecorderImpl(this.instruments);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
|
2
2
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
3
|
import type { Logger } from '@aztec/foundation/log';
|
|
4
4
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
5
|
-
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
6
5
|
import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
7
6
|
import type { ValidatorClient } from '@aztec/validator-client';
|
|
8
7
|
import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
|
|
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
|
|
|
18
17
|
* Handles governance and slashing voting for a given slot.
|
|
19
18
|
*/
|
|
20
19
|
export class CheckpointVoter {
|
|
21
|
-
private slotTimestamp: bigint;
|
|
22
20
|
private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
23
21
|
private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
24
22
|
|
|
@@ -33,8 +31,6 @@ export class CheckpointVoter {
|
|
|
33
31
|
private readonly metrics: SequencerMetrics,
|
|
34
32
|
private readonly log: Logger,
|
|
35
33
|
) {
|
|
36
|
-
this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
|
|
37
|
-
|
|
38
34
|
// Create separate signers with appropriate duty contexts for governance and slashing votes
|
|
39
35
|
// These use HA protection to ensure only one node signs per slot/duty
|
|
40
36
|
const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
|
|
@@ -77,7 +73,6 @@ export class CheckpointVoter {
|
|
|
77
73
|
return await this.publisher.enqueueGovernanceCastSignal(
|
|
78
74
|
governanceProposerPayload,
|
|
79
75
|
this.slot,
|
|
80
|
-
this.slotTimestamp,
|
|
81
76
|
this.attestorAddress,
|
|
82
77
|
this.governanceSigner,
|
|
83
78
|
);
|
|
@@ -108,13 +103,7 @@ export class CheckpointVoter {
|
|
|
108
103
|
|
|
109
104
|
this.metrics.recordSlashingAttempt(actions.length);
|
|
110
105
|
|
|
111
|
-
return await this.publisher.enqueueSlashingActions(
|
|
112
|
-
actions,
|
|
113
|
-
this.slot,
|
|
114
|
-
this.slotTimestamp,
|
|
115
|
-
this.attestorAddress,
|
|
116
|
-
this.slashingSigner,
|
|
117
|
-
);
|
|
106
|
+
return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
|
|
118
107
|
} catch (err) {
|
|
119
108
|
if (err instanceof DutyAlreadySignedError) {
|
|
120
109
|
this.log.info(`Slashing vote already signed by another node`, {
|
package/src/sequencer/events.ts
CHANGED
|
@@ -1,20 +1,76 @@
|
|
|
1
|
-
import type { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
|
+
import type { BlockNumber, CheckpointNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import type { BlockHash } from '@aztec/stdlib/block';
|
|
2
3
|
|
|
3
4
|
import type { Action } from '../publisher/sequencer-publisher.js';
|
|
4
5
|
import type { SequencerState } from './utils.js';
|
|
5
6
|
|
|
6
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
|
+
*/
|
|
7
21
|
['state-changed']: (args: {
|
|
8
22
|
oldState: SequencerState;
|
|
9
23
|
newState: SequencerState;
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
secondsIntoBuildFrame?: number;
|
|
25
|
+
targetSlot?: SlotNumber;
|
|
26
|
+
}) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Emitted by the sequencer once it has decided it is going to attempt to build a
|
|
29
|
+
* checkpoint for `targetSlot`, after computing the L1 simulation overrides used by
|
|
30
|
+
* `canProposeAt`. Fired BEFORE the L1 simulation is run, so consumers can observe the
|
|
31
|
+
* decision regardless of whether the propose ultimately lands.
|
|
32
|
+
*
|
|
33
|
+
* - `hadProposedParent` indicates whether the build saw a proposed (pipelined) parent
|
|
34
|
+
* checkpoint that hasn't landed on L1 yet.
|
|
35
|
+
* - `provenOverride` is the assumed proven checkpoint number pinned for the L1
|
|
36
|
+
* simulation. The plan always pins both chain tips to short-circuit `canPruneAtTime`,
|
|
37
|
+
* so this is populated whenever a simulation plan was built — the value either
|
|
38
|
+
* matches the on-chain proven snapshot (defensive pin) or the assumed-proven
|
|
39
|
+
* checkpoint when building optimistically across a pruning boundary.
|
|
40
|
+
* - `simulatedPending` is the pending checkpoint passed to L1 simulation. The plan
|
|
41
|
+
* always pins both chain tips to short-circuit `canPruneAtTime`, so this reflects
|
|
42
|
+
* either the pipelined/invalidated tip or the on-chain pending snapshot.
|
|
43
|
+
*/
|
|
44
|
+
['preparing-checkpoint']: (args: {
|
|
45
|
+
targetSlot: SlotNumber;
|
|
46
|
+
checkpointNumber: CheckpointNumber;
|
|
47
|
+
hadProposedParent: boolean;
|
|
48
|
+
provenOverride: CheckpointNumber | undefined;
|
|
49
|
+
simulatedPending: CheckpointNumber | undefined;
|
|
12
50
|
}) => void;
|
|
13
51
|
['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
14
|
-
['block-tx-count-check-failed']: (args: { minTxs: number;
|
|
52
|
+
['block-tx-count-check-failed']: (args: { minTxs: number; slot: SlotNumber }) => void;
|
|
15
53
|
['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
16
|
-
['block-proposed']: (args: {
|
|
54
|
+
['block-proposed']: (args: {
|
|
55
|
+
blockNumber: BlockNumber;
|
|
56
|
+
blockHash: BlockHash;
|
|
57
|
+
checkpointNumber: CheckpointNumber;
|
|
58
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
59
|
+
slot: SlotNumber;
|
|
60
|
+
buildSlot: SlotNumber;
|
|
61
|
+
}) => void;
|
|
17
62
|
['checkpoint-empty']: (args: { slot: SlotNumber }) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Emitted when the proposer's pre-broadcast `validateBlockHeader` simulation fails. This is a
|
|
65
|
+
* last-chance check before we gossip a checkpoint proposal: a failure here means the header
|
|
66
|
+
* would not be accepted by L1 (e.g. archive mismatch, stale chain tip, or some other state
|
|
67
|
+
* drift between when we built the checkpoint and when we are about to broadcast it).
|
|
68
|
+
*/
|
|
69
|
+
['header-validation-failed']: (args: {
|
|
70
|
+
slot: SlotNumber;
|
|
71
|
+
checkpointNumber: CheckpointNumber;
|
|
72
|
+
reason: string;
|
|
73
|
+
}) => void;
|
|
18
74
|
['checkpoint-publish-failed']: (args: {
|
|
19
75
|
slot: SlotNumber;
|
|
20
76
|
successfulActions?: Action[];
|
|
@@ -24,4 +80,9 @@ export type SequencerEvents = {
|
|
|
24
80
|
}) => void;
|
|
25
81
|
['checkpoint-published']: (args: { checkpoint: CheckpointNumber; slot: SlotNumber }) => void;
|
|
26
82
|
['checkpoint-error']: (args: { error: Error }) => void;
|
|
83
|
+
['pipelined-checkpoint-discarded']: (args: {
|
|
84
|
+
slot: SlotNumber;
|
|
85
|
+
checkpointNumber: CheckpointNumber;
|
|
86
|
+
reason: string;
|
|
87
|
+
}) => void;
|
|
27
88
|
};
|
package/src/sequencer/metrics.ts
CHANGED
|
@@ -26,6 +26,7 @@ export class SequencerMetrics {
|
|
|
26
26
|
private blockBuildDuration: Histogram;
|
|
27
27
|
private blockBuildManaPerSecond: Gauge;
|
|
28
28
|
private stateTransitionBufferDuration: Histogram;
|
|
29
|
+
private stateDuration: Histogram;
|
|
29
30
|
|
|
30
31
|
// these are gauges because for individual sequencers building a block is not something that happens often enough to warrant a histogram
|
|
31
32
|
private timeToCollectAttestations: Gauge;
|
|
@@ -44,13 +45,9 @@ export class SequencerMetrics {
|
|
|
44
45
|
private checkpointProposalFailed: UpDownCounter;
|
|
45
46
|
private checkpointSuccess: UpDownCounter;
|
|
46
47
|
private slashingAttempts: UpDownCounter;
|
|
47
|
-
private checkpointAttestationDelay: Histogram;
|
|
48
|
-
private checkpointBuildDuration: Histogram;
|
|
49
|
-
private checkpointBlockCount: Gauge;
|
|
50
|
-
private checkpointTxCount: Gauge;
|
|
51
|
-
private checkpointTotalMana: Gauge;
|
|
52
48
|
private pipelineDepth: Gauge;
|
|
53
49
|
private pipelineDiscards: UpDownCounter;
|
|
50
|
+
private pipelineParentCheckpointMismatches: UpDownCounter;
|
|
54
51
|
|
|
55
52
|
// Fisherman fee analysis metrics
|
|
56
53
|
private fishermanWouldBeIncluded: UpDownCounter;
|
|
@@ -68,6 +65,10 @@ export class SequencerMetrics {
|
|
|
68
65
|
private fishermanMinedBlobTxPriorityFee: Histogram;
|
|
69
66
|
private fishermanMinedBlobTxTotalCost: Histogram;
|
|
70
67
|
|
|
68
|
+
private blockInterBlockTime: Histogram;
|
|
69
|
+
private lastBlockBuiltTimestamp?: number;
|
|
70
|
+
private lastBlockBuiltSlot?: SlotNumber;
|
|
71
|
+
|
|
71
72
|
private lastSeenSlot?: SlotNumber;
|
|
72
73
|
|
|
73
74
|
constructor(
|
|
@@ -86,9 +87,11 @@ export class SequencerMetrics {
|
|
|
86
87
|
|
|
87
88
|
this.blockBuildManaPerSecond = this.meter.createGauge(Metrics.SEQUENCER_BLOCK_BUILD_MANA_PER_SECOND);
|
|
88
89
|
|
|
90
|
+
this.blockInterBlockTime = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_INTER_BLOCK_TIME);
|
|
91
|
+
|
|
89
92
|
this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
|
|
90
93
|
|
|
91
|
-
this.
|
|
94
|
+
this.stateDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_DURATION);
|
|
92
95
|
|
|
93
96
|
this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
|
|
94
97
|
|
|
@@ -138,15 +141,23 @@ export class SequencerMetrics {
|
|
|
138
141
|
Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
|
|
139
142
|
);
|
|
140
143
|
|
|
141
|
-
this.checkpointBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION);
|
|
142
|
-
this.checkpointBlockCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT);
|
|
143
|
-
this.checkpointTxCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT);
|
|
144
|
-
this.checkpointTotalMana = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA);
|
|
145
|
-
|
|
146
144
|
this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
|
|
147
145
|
|
|
148
146
|
this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
|
|
149
147
|
this.pipelineDiscards = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_PIPELINE_DISCARDS_COUNT);
|
|
148
|
+
this.pipelineParentCheckpointMismatches = createUpDownCounterWithDefault(
|
|
149
|
+
this.meter,
|
|
150
|
+
Metrics.SEQUENCER_PIPELINE_PARENT_CHECKPOINT_MISMATCH_COUNT,
|
|
151
|
+
{
|
|
152
|
+
[Attributes.ERROR_TYPE]: [
|
|
153
|
+
'archiver-sync-timeout',
|
|
154
|
+
'parent-not-on-l1',
|
|
155
|
+
'parent-hash-mismatch',
|
|
156
|
+
'parent-invalid-attestations',
|
|
157
|
+
'unexpected-parent-appeared',
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
);
|
|
150
161
|
this.pipelineDepth.record(0);
|
|
151
162
|
|
|
152
163
|
// Fisherman fee analysis metrics
|
|
@@ -211,21 +222,25 @@ export class SequencerMetrics {
|
|
|
211
222
|
this.timeToCollectAttestations.record(0);
|
|
212
223
|
}
|
|
213
224
|
|
|
214
|
-
public recordCheckpointAttestationDelay(duration: number) {
|
|
215
|
-
this.checkpointAttestationDelay.record(duration);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
225
|
public recordCollectedAttestations(count: number, durationMs: number) {
|
|
219
226
|
this.collectedAttestions.record(count);
|
|
220
227
|
this.timeToCollectAttestations.record(Math.ceil(durationMs));
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
recordBuiltBlock(buildDurationMs: number, totalMana: number) {
|
|
230
|
+
recordBuiltBlock(buildDurationMs: number, totalMana: number, slot: SlotNumber) {
|
|
224
231
|
this.blockCounter.add(1, {
|
|
225
232
|
[Attributes.STATUS]: 'built',
|
|
226
233
|
});
|
|
227
234
|
this.blockBuildDuration.record(Math.ceil(buildDurationMs));
|
|
228
235
|
this.blockBuildManaPerSecond.record(Math.ceil((totalMana * 1000) / buildDurationMs));
|
|
236
|
+
|
|
237
|
+
// Only record inter-block time between blocks built within the same slot.
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
if (this.lastBlockBuiltTimestamp !== undefined && this.lastBlockBuiltSlot === slot) {
|
|
240
|
+
this.blockInterBlockTime.record(now - this.lastBlockBuiltTimestamp);
|
|
241
|
+
}
|
|
242
|
+
this.lastBlockBuiltTimestamp = now;
|
|
243
|
+
this.lastBlockBuiltSlot = slot;
|
|
229
244
|
}
|
|
230
245
|
|
|
231
246
|
recordFailedBlock() {
|
|
@@ -240,6 +255,12 @@ export class SequencerMetrics {
|
|
|
240
255
|
});
|
|
241
256
|
}
|
|
242
257
|
|
|
258
|
+
recordStateDuration(durationMs: number, state: SequencerState) {
|
|
259
|
+
this.stateDuration.record(Math.ceil(durationMs), {
|
|
260
|
+
[Attributes.SEQUENCER_STATE]: state,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
243
264
|
recordPipelineDepth(depth: number) {
|
|
244
265
|
this.pipelineDepth.record(depth);
|
|
245
266
|
}
|
|
@@ -248,6 +269,12 @@ export class SequencerMetrics {
|
|
|
248
269
|
this.pipelineDiscards.add(count);
|
|
249
270
|
}
|
|
250
271
|
|
|
272
|
+
recordPipelineParentCheckpointMismatch(reason: string) {
|
|
273
|
+
this.pipelineParentCheckpointMismatches.add(1, {
|
|
274
|
+
[Attributes.ERROR_TYPE]: reason,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
251
278
|
incOpenSlot(slot: SlotNumber, proposer: string) {
|
|
252
279
|
// sequencer went through the loop a second time. Noop
|
|
253
280
|
if (slot === this.lastSeenSlot) {
|
|
@@ -306,14 +333,6 @@ export class SequencerMetrics {
|
|
|
306
333
|
});
|
|
307
334
|
}
|
|
308
335
|
|
|
309
|
-
/** Records aggregate metrics for a completed checkpoint build. */
|
|
310
|
-
recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
|
|
311
|
-
this.checkpointBuildDuration.record(Math.ceil(durationMs));
|
|
312
|
-
this.checkpointBlockCount.record(blockCount);
|
|
313
|
-
this.checkpointTxCount.record(txCount);
|
|
314
|
-
this.checkpointTotalMana.record(totalMana);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
336
|
recordSlashingAttempt(actionCount: number) {
|
|
318
337
|
this.slashingAttempts.add(actionCount);
|
|
319
338
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import type { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { EpochNumber, type SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { timesParallel } from '@aztec/foundation/collection';
|
|
5
|
+
import type { Logger } from '@aztec/foundation/log';
|
|
6
|
+
import { formatSeconds } from '@aztec/foundation/string';
|
|
7
|
+
import type { DateProvider } from '@aztec/foundation/timer';
|
|
8
|
+
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
9
|
+
import { type L1RollupConstants, getStartTimestampForEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
10
|
+
|
|
11
|
+
/** Collaborators {@link logMissingCommittee} needs to diagnose and report why no committee exists. */
|
|
12
|
+
export interface MissingCommitteeContext {
|
|
13
|
+
epochCache: Pick<EpochCache, 'getL1Constants' | 'getLagInEpochsForValidatorSet'>;
|
|
14
|
+
rollupContract: Pick<RollupContract, 'getActiveAttesterCount' | 'getAttesterCountAtTime'>;
|
|
15
|
+
l2BlockSource: Pick<L2BlockSource, 'getBlockNumber'>;
|
|
16
|
+
l1Constants: Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration' | 'epochDuration'>;
|
|
17
|
+
dateProvider: Pick<DateProvider, 'nowInSeconds'>;
|
|
18
|
+
logger: Pick<Logger, 'info' | 'warn' | 'debug'>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Why the sequencer cannot find a validator committee for an upcoming slot. A committee for an epoch is
|
|
23
|
+
* sampled from the validator set as of `lagInEpochsForValidatorSet` epochs earlier, and L1 refuses to
|
|
24
|
+
* produce one until at least `targetCommitteeSize` validators were staked at that sampling time.
|
|
25
|
+
*/
|
|
26
|
+
export type MissingCommitteeCause =
|
|
27
|
+
/** Enough validators are staked now; the committee is just waiting for the sampling window to advance. */
|
|
28
|
+
| 'awaiting-sampling-lag'
|
|
29
|
+
/** The chain has not produced any block yet and too few validators are staked: normal bootstrap state. */
|
|
30
|
+
| 'awaiting-first-validators'
|
|
31
|
+
/** The chain has produced blocks before but the validator set has since dropped below the required size. */
|
|
32
|
+
| 'validator-set-shrank';
|
|
33
|
+
|
|
34
|
+
export interface MissingCommitteeDiagnosis {
|
|
35
|
+
cause: MissingCommitteeCause;
|
|
36
|
+
/** Log severity: bootstrap and lag are expected (`info`); a shrunken set on a live chain is not (`warn`). */
|
|
37
|
+
severity: 'info' | 'warn';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Logs why no validator committee exists for the given slot. Rather than the cryptic "committee does not
|
|
42
|
+
* exist on L1", it diagnoses the cause from the live attester count and whether the chain has ever produced
|
|
43
|
+
* a block: a bootstrapping chain waiting for validators to stake, a full set still waiting for the sampling
|
|
44
|
+
* lag to elapse, or a validator set that has shrunk below the required size on a live chain. Only the last is
|
|
45
|
+
* a genuine problem, so only it is logged at `warn`.
|
|
46
|
+
*
|
|
47
|
+
* Best-effort: if the diagnostic L1 reads fail we fall back to a neutral message so this never throws on the
|
|
48
|
+
* propose path.
|
|
49
|
+
*/
|
|
50
|
+
export async function logMissingCommittee(
|
|
51
|
+
targetSlot: SlotNumber,
|
|
52
|
+
targetEpoch: EpochNumber,
|
|
53
|
+
ctx: MissingCommitteeContext,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const targetCommitteeSize = ctx.epochCache.getL1Constants().targetCommitteeSize;
|
|
56
|
+
const lag = ctx.epochCache.getLagInEpochsForValidatorSet();
|
|
57
|
+
|
|
58
|
+
let attesterCount: number;
|
|
59
|
+
let hasProducedBlocks: boolean;
|
|
60
|
+
try {
|
|
61
|
+
[attesterCount, hasProducedBlocks] = await Promise.all([
|
|
62
|
+
ctx.rollupContract.getActiveAttesterCount(),
|
|
63
|
+
ctx.l2BlockSource.getBlockNumber().then(n => n > 0),
|
|
64
|
+
]);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
ctx.logger.warn(`No committee found for slot ${targetSlot}; could not determine validator set size`, {
|
|
67
|
+
targetSlot,
|
|
68
|
+
targetEpoch,
|
|
69
|
+
targetCommitteeSize,
|
|
70
|
+
err,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const diagnosis = classifyMissingCommittee({ attesterCount, targetCommitteeSize, hasProducedBlocks });
|
|
76
|
+
const logCtx = { targetSlot, targetEpoch, attesterCount, targetCommitteeSize, cause: diagnosis.cause };
|
|
77
|
+
|
|
78
|
+
switch (diagnosis.cause) {
|
|
79
|
+
case 'awaiting-sampling-lag': {
|
|
80
|
+
const firstCommitteeEpoch = await estimateFirstCommitteeEpoch(targetEpoch, lag, targetCommitteeSize, ctx);
|
|
81
|
+
const staked = `${attesterCount} validators are staked (>= ${targetCommitteeSize} required)`;
|
|
82
|
+
const forming = `the committee is still forming as the ${lag}-epoch sampling window advances`;
|
|
83
|
+
// When we could not pin the exact epoch, report the safe upper bound rather than a misleading ETA.
|
|
84
|
+
const epoch = firstCommitteeEpoch ?? EpochNumber(targetEpoch + lag + 1);
|
|
85
|
+
const expectation =
|
|
86
|
+
firstCommitteeEpoch === undefined
|
|
87
|
+
? `A committee should exist no later than epoch ${epoch}.`
|
|
88
|
+
: `The first committee is expected at epoch ${epoch}${formatEpochEta(epoch, ctx)}.`;
|
|
89
|
+
ctx.logger.info(`No committee for slot ${targetSlot} yet: ${staked}, ${forming}. ${expectation}`, {
|
|
90
|
+
...logCtx,
|
|
91
|
+
firstCommitteeEpoch: epoch,
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'awaiting-first-validators':
|
|
96
|
+
ctx.logger.info(
|
|
97
|
+
`No committee for slot ${targetSlot}: the chain has not started producing blocks and only ${attesterCount} ` +
|
|
98
|
+
`of the ${targetCommitteeSize} required validators are staked. Block production begins once at least ` +
|
|
99
|
+
`${targetCommitteeSize} validators stake and the ${lag}-epoch sampling lag elapses.`,
|
|
100
|
+
logCtx,
|
|
101
|
+
);
|
|
102
|
+
break;
|
|
103
|
+
case 'validator-set-shrank':
|
|
104
|
+
ctx.logger.warn(
|
|
105
|
+
`No committee for slot ${targetSlot}: the validator set has dropped to ${attesterCount}, below the ` +
|
|
106
|
+
`${targetCommitteeSize} required to form a committee. The chain cannot progress until enough validators ` +
|
|
107
|
+
`are staked again — check for validators exiting or being slashed.`,
|
|
108
|
+
logCtx,
|
|
109
|
+
);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Classifies why no committee exists for a slot, using only cheap live signals: the current attester count
|
|
116
|
+
* and whether the chain has ever produced a block. See {@link MissingCommitteeCause} for the reasoning.
|
|
117
|
+
*/
|
|
118
|
+
export function classifyMissingCommittee(args: {
|
|
119
|
+
attesterCount: number;
|
|
120
|
+
targetCommitteeSize: number;
|
|
121
|
+
hasProducedBlocks: boolean;
|
|
122
|
+
}): MissingCommitteeDiagnosis {
|
|
123
|
+
const { attesterCount, targetCommitteeSize, hasProducedBlocks } = args;
|
|
124
|
+
|
|
125
|
+
if (attesterCount >= targetCommitteeSize) {
|
|
126
|
+
return { cause: 'awaiting-sampling-lag', severity: 'info' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (hasProducedBlocks) {
|
|
130
|
+
return { cause: 'validator-set-shrank', severity: 'warn' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { cause: 'awaiting-first-validators', severity: 'info' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Given each candidate epoch after the one we failed to propose at, paired with the attester count staked at
|
|
138
|
+
* that epoch's validator-set sample time (`undefined` when the sample time is still in the future), returns
|
|
139
|
+
* the earliest epoch that will have a committee, or `undefined` if none qualifies.
|
|
140
|
+
*
|
|
141
|
+
* Candidates must be ordered ascending by epoch. A committee for epoch `E` exists iff at least
|
|
142
|
+
* `targetCommitteeSize` validators were staked at `E`'s sample time, so the first candidate whose sampled
|
|
143
|
+
* count meets the target is the answer; a candidate whose sample time is still in the future is assumed to
|
|
144
|
+
* have a committee if the set holds and wins as soon as it is reached. The predicate is not monotone in `E`
|
|
145
|
+
* (the set can dip below target between sample times), so this must scan ascending rather than binary-search.
|
|
146
|
+
*/
|
|
147
|
+
export function findFirstEpochWithCommittee(args: {
|
|
148
|
+
candidates: { epoch: EpochNumber; sampledAttesterCount: number | undefined }[];
|
|
149
|
+
targetCommitteeSize: number;
|
|
150
|
+
}): EpochNumber | undefined {
|
|
151
|
+
return args.candidates.find(
|
|
152
|
+
c => c.sampledAttesterCount === undefined || c.sampledAttesterCount >= args.targetCommitteeSize,
|
|
153
|
+
)?.epoch;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Earliest epoch expected to have a committee, replaying L1's validator-set sampling rule: a committee for
|
|
158
|
+
* epoch `E` exists iff at least `targetCommitteeSize` attesters were staked at `E`'s sample time
|
|
159
|
+
* (`epochStart(E) - lag * epochDuration`). Only epochs after the one we failed at can qualify, and the set
|
|
160
|
+
* must have crossed the target within the last `lag` epochs' sample window, so the answer lies in
|
|
161
|
+
* `(targetEpoch, targetEpoch + lag + 1]`. Reads the historical attester count at each candidate's sample time
|
|
162
|
+
* (at most `lag` on-chain reads; future sample times are assumed to hold). Returns `undefined` if no candidate
|
|
163
|
+
* qualifies or the reads fail, leaving the caller to report a bound instead of an ETA.
|
|
164
|
+
*/
|
|
165
|
+
async function estimateFirstCommitteeEpoch(
|
|
166
|
+
targetEpoch: EpochNumber,
|
|
167
|
+
lag: number,
|
|
168
|
+
targetCommitteeSize: number,
|
|
169
|
+
ctx: MissingCommitteeContext,
|
|
170
|
+
): Promise<EpochNumber | undefined> {
|
|
171
|
+
try {
|
|
172
|
+
const epochDurationSeconds = BigInt(ctx.l1Constants.epochDuration * ctx.l1Constants.slotDuration);
|
|
173
|
+
const nowSeconds = BigInt(ctx.dateProvider.nowInSeconds());
|
|
174
|
+
const candidates = await timesParallel(lag + 1, async i => {
|
|
175
|
+
const epoch = EpochNumber(targetEpoch + 1 + i);
|
|
176
|
+
const sampleTime = getStartTimestampForEpoch(epoch, ctx.l1Constants) - epochDurationSeconds * BigInt(lag);
|
|
177
|
+
const sampledAttesterCount =
|
|
178
|
+
sampleTime > nowSeconds ? undefined : await ctx.rollupContract.getAttesterCountAtTime(sampleTime);
|
|
179
|
+
return { epoch, sampledAttesterCount };
|
|
180
|
+
});
|
|
181
|
+
return findFirstEpochWithCommittee({ candidates, targetCommitteeSize });
|
|
182
|
+
} catch (err) {
|
|
183
|
+
ctx.logger.debug(`Could not estimate first committee epoch after epoch ${targetEpoch}`, { targetEpoch, err });
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Formats the ETA to the start of the given epoch as ` (~12m from now)`, or empty if it is in the past. */
|
|
189
|
+
function formatEpochEta(epoch: EpochNumber, ctx: MissingCommitteeContext): string {
|
|
190
|
+
const secondsUntil = Number(getStartTimestampForEpoch(epoch, ctx.l1Constants)) - ctx.dateProvider.nowInSeconds();
|
|
191
|
+
return secondsUntil > 0 ? ` (~${formatSeconds(secondsUntil)} from now)` : '';
|
|
192
|
+
}
|
|
@@ -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
|
+
}
|