@aztec/sequencer-client 0.0.1-commit.db765a8 → 0.0.1-commit.ddcf04837
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 +6 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +49 -27
- package/dest/config.d.ts +25 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +26 -20
- 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 +128 -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 +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +15 -14
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +16 -50
- package/dest/global_variable_builder/index.d.ts +4 -2
- 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 +13 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +19 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +2 -3
- package/dest/publisher/sequencer-publisher.d.ts +51 -43
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +135 -111
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +26 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +318 -201
- 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 +2 -1
- 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 +23 -20
- package/dest/sequencer/sequencer.d.ts +26 -10
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +136 -73
- package/dest/sequencer/timetable.d.ts +17 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -43
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +7 -9
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +39 -30
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +61 -28
- package/src/config.ts +33 -22
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +26 -62
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +38 -4
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-publisher-factory.ts +3 -6
- package/src/publisher/sequencer-publisher.ts +202 -158
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +408 -232
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +29 -25
- package/src/sequencer/sequencer.ts +197 -80
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +51 -48
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
|
|
4
3
|
import {
|
|
5
4
|
BlockNumber,
|
|
6
5
|
CheckpointNumber,
|
|
@@ -32,17 +31,22 @@ import {
|
|
|
32
31
|
type L2BlockSource,
|
|
33
32
|
MaliciousCommitteeAttestationsAndSigners,
|
|
34
33
|
} from '@aztec/stdlib/block';
|
|
35
|
-
import type
|
|
36
|
-
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
34
|
+
import { type Checkpoint, type ProposedCheckpointData, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
35
|
+
import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
37
36
|
import { Gas } from '@aztec/stdlib/gas';
|
|
38
37
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
type BlockBuilderOptions,
|
|
39
|
+
InsufficientValidTxsError,
|
|
41
40
|
type ResolvedSequencerConfig,
|
|
42
41
|
type WorldStateSynchronizer,
|
|
43
42
|
} from '@aztec/stdlib/interfaces/server';
|
|
44
43
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
45
|
-
import type {
|
|
44
|
+
import type {
|
|
45
|
+
BlockProposal,
|
|
46
|
+
BlockProposalOptions,
|
|
47
|
+
CheckpointProposal,
|
|
48
|
+
CheckpointProposalOptions,
|
|
49
|
+
} from '@aztec/stdlib/p2p';
|
|
46
50
|
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
47
51
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
48
52
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
@@ -53,6 +57,11 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
53
57
|
|
|
54
58
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
55
59
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
60
|
+
import {
|
|
61
|
+
buildPipelinedParentSimulationOverridesPlan,
|
|
62
|
+
buildSubmissionSimulationOverridesPlan,
|
|
63
|
+
} from './chain_state_overrides.js';
|
|
64
|
+
import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
|
|
56
65
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
57
66
|
import { SequencerInterruptedError } from './errors.js';
|
|
58
67
|
import type { SequencerEvents } from './events.js';
|
|
@@ -64,6 +73,20 @@ import { SequencerState } from './utils.js';
|
|
|
64
73
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
65
74
|
const TXS_POLLING_MS = 500;
|
|
66
75
|
|
|
76
|
+
/** Result from proposeCheckpoint when a checkpoint was successfully built and broadcast. */
|
|
77
|
+
type CheckpointProposalBroadcast = {
|
|
78
|
+
checkpoint: Checkpoint;
|
|
79
|
+
proposal: CheckpointProposal;
|
|
80
|
+
blockProposedAt: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Result after attestation collection and signing, ready for L1 submission. */
|
|
84
|
+
type CheckpointProposalResult = {
|
|
85
|
+
checkpoint: Checkpoint;
|
|
86
|
+
attestations: CommitteeAttestationsAndSigners;
|
|
87
|
+
attestationsSignature: Signature;
|
|
88
|
+
};
|
|
89
|
+
|
|
67
90
|
/**
|
|
68
91
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
69
92
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -73,9 +96,16 @@ const TXS_POLLING_MS = 500;
|
|
|
73
96
|
export class CheckpointProposalJob implements Traceable {
|
|
74
97
|
protected readonly log: Logger;
|
|
75
98
|
|
|
99
|
+
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
100
|
+
private pendingL1Submission: Promise<void> | undefined;
|
|
101
|
+
|
|
102
|
+
/** Pipelined parent chain state used while building and later submitting this checkpoint. */
|
|
103
|
+
private pipelinedParentSimulationOverridesPlan?: SimulationOverridesPlan;
|
|
104
|
+
|
|
76
105
|
constructor(
|
|
77
|
-
private readonly
|
|
78
|
-
private readonly
|
|
106
|
+
private readonly slotNow: SlotNumber,
|
|
107
|
+
private readonly targetSlot: SlotNumber,
|
|
108
|
+
private readonly targetEpoch: EpochNumber,
|
|
79
109
|
private readonly checkpointNumber: CheckpointNumber,
|
|
80
110
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
81
111
|
// TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
@@ -98,17 +128,31 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
98
128
|
private readonly epochCache: EpochCache,
|
|
99
129
|
private readonly dateProvider: DateProvider,
|
|
100
130
|
private readonly metrics: SequencerMetrics,
|
|
131
|
+
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
101
132
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
102
133
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
103
134
|
public readonly tracer: Tracer,
|
|
104
135
|
bindings?: LoggerBindings,
|
|
136
|
+
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
105
137
|
) {
|
|
106
|
-
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
138
|
+
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
139
|
+
...bindings,
|
|
140
|
+
instanceId: `slot-${this.slotNow}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
145
|
+
public async awaitPendingSubmission(): Promise<void> {
|
|
146
|
+
this.log.info('Awaiting pending L1 payload submission');
|
|
147
|
+
await this.pendingL1Submission;
|
|
107
148
|
}
|
|
108
149
|
|
|
109
150
|
/**
|
|
110
151
|
* Executes the checkpoint proposal job.
|
|
111
|
-
*
|
|
152
|
+
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
153
|
+
* Attestation collection, signing, and L1 submission are backgrounded so the
|
|
154
|
+
* work loop can return to IDLE immediately for consecutive slot proposals.
|
|
155
|
+
* Returns the built checkpoint if successful, undefined otherwise.
|
|
112
156
|
*/
|
|
113
157
|
@trackSpan('CheckpointProposalJob.execute')
|
|
114
158
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
@@ -116,7 +160,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
116
160
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
117
161
|
// These are constant for the whole slot, so we only enqueue them once
|
|
118
162
|
const votesPromises = new CheckpointVoter(
|
|
119
|
-
this.
|
|
163
|
+
this.targetSlot,
|
|
120
164
|
this.publisher,
|
|
121
165
|
this.attestorAddress,
|
|
122
166
|
this.validatorClient,
|
|
@@ -127,63 +171,195 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
127
171
|
this.log,
|
|
128
172
|
).enqueueVotes();
|
|
129
173
|
|
|
130
|
-
// Build
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
134
|
-
await Promise.all(votesPromises);
|
|
174
|
+
// Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
|
|
175
|
+
// Returns after broadcast — attestation collection is deferred.
|
|
176
|
+
const broadcast = await this.proposeCheckpoint();
|
|
135
177
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
178
|
+
if (!broadcast) {
|
|
179
|
+
await Promise.all(votesPromises);
|
|
180
|
+
// Still submit votes even without a checkpoint
|
|
181
|
+
if (!this.config.fishermanMode) {
|
|
182
|
+
this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(() => {});
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
138
185
|
}
|
|
139
186
|
|
|
187
|
+
const { checkpoint } = broadcast;
|
|
188
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
189
|
+
|
|
140
190
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
141
191
|
if (this.config.fishermanMode) {
|
|
142
192
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
143
|
-
return;
|
|
193
|
+
return checkpoint;
|
|
144
194
|
}
|
|
145
195
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
197
|
+
this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
|
|
198
|
+
|
|
199
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
200
|
+
return checkpoint;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
205
|
+
* Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
|
|
206
|
+
*/
|
|
207
|
+
private async waitForAttestationsAndEnqueueSubmissionAsync(
|
|
208
|
+
broadcast: CheckpointProposalBroadcast,
|
|
209
|
+
votesPromises: Promise<unknown>[],
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const { checkpoint, proposal, blockProposedAt } = broadcast;
|
|
212
|
+
try {
|
|
213
|
+
await Promise.all(votesPromises);
|
|
214
|
+
|
|
215
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
216
|
+
const attestations = await this.waitForAttestations(proposal);
|
|
217
|
+
|
|
218
|
+
this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
|
|
219
|
+
|
|
220
|
+
// Proposer must sign over the attestations before pushing them to L1
|
|
221
|
+
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
222
|
+
let attestationsSignature: Signature;
|
|
223
|
+
try {
|
|
224
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
225
|
+
attestations,
|
|
226
|
+
signer,
|
|
227
|
+
this.targetSlot,
|
|
228
|
+
this.checkpointNumber,
|
|
229
|
+
);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Enqueue the checkpoint for L1 submission
|
|
238
|
+
await this.enqueueCheckpointForSubmission({ checkpoint, attestations, attestationsSignature });
|
|
239
|
+
|
|
240
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
241
|
+
const submitAfter = this.epochCache.isProposerPipeliningEnabled()
|
|
242
|
+
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
243
|
+
: new Date(this.dateProvider.now());
|
|
244
|
+
|
|
245
|
+
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
246
|
+
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
247
|
+
if (proposedAction) {
|
|
248
|
+
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
249
|
+
const coinbase = checkpoint.header.coinbase;
|
|
250
|
+
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
251
|
+
} else {
|
|
252
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
253
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
254
|
+
this.metrics.recordPipelineDiscard();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof SequencerInterruptedError) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
|
|
262
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
263
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
264
|
+
this.metrics.recordPipelineDiscard();
|
|
265
|
+
}
|
|
157
266
|
}
|
|
158
267
|
}
|
|
159
268
|
|
|
269
|
+
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
|
|
270
|
+
private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
|
|
271
|
+
const { checkpoint, attestations, attestationsSignature } = result;
|
|
272
|
+
|
|
273
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
274
|
+
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
275
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
276
|
+
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
277
|
+
|
|
278
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
279
|
+
if (
|
|
280
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
281
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
282
|
+
) {
|
|
283
|
+
const roll = Math.max(0, randomInt(100));
|
|
284
|
+
if (roll < this.config.skipPublishingCheckpointsPercent) {
|
|
285
|
+
this.log.warn(
|
|
286
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`,
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
293
|
+
const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
|
|
294
|
+
pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
|
|
295
|
+
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
296
|
+
lastArchiveRoot: checkpoint.header.lastArchiveRoot,
|
|
297
|
+
pipeliningEnabled: isPipelining,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
301
|
+
txTimeoutAt,
|
|
302
|
+
...(submissionSimulationOverridesPlan ? { simulationOverridesPlan: submissionSimulationOverridesPlan } : {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
160
306
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
161
307
|
return {
|
|
162
308
|
// nullish operator needed for tests
|
|
163
309
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
164
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
310
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
165
311
|
};
|
|
166
312
|
})
|
|
167
|
-
private async proposeCheckpoint(): Promise<
|
|
313
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
|
|
168
314
|
try {
|
|
315
|
+
const now = this.dateProvider.now();
|
|
316
|
+
if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
|
|
317
|
+
// Measure against the wall-clock slot whose build window we are currently using.
|
|
318
|
+
// In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
|
|
319
|
+
// target-slot boundary a full slot away from the actual build start time.
|
|
320
|
+
const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
|
|
321
|
+
this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
|
|
322
|
+
}
|
|
323
|
+
this.checkpointMetrics.startCheckpointTiming(now);
|
|
324
|
+
|
|
169
325
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
170
326
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
171
327
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
172
328
|
|
|
173
329
|
// Start the checkpoint
|
|
174
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
175
|
-
this.
|
|
330
|
+
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
331
|
+
this.log.info(`Starting checkpoint proposal`, {
|
|
332
|
+
buildSlot: this.slotNow,
|
|
333
|
+
submissionSlot: this.targetSlot,
|
|
334
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
335
|
+
proposer: this.proposer?.toString(),
|
|
336
|
+
coinbase: coinbase.toString(),
|
|
337
|
+
});
|
|
338
|
+
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
176
339
|
|
|
177
340
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
178
341
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
179
342
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
180
343
|
}
|
|
181
344
|
|
|
182
|
-
// Create checkpoint builder for the slot
|
|
345
|
+
// Create checkpoint builder for the slot.
|
|
346
|
+
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
347
|
+
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
348
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
349
|
+
this.pipelinedParentSimulationOverridesPlan = isPipelining
|
|
350
|
+
? await buildPipelinedParentSimulationOverridesPlan({
|
|
351
|
+
checkpointNumber: this.checkpointNumber,
|
|
352
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
353
|
+
rollup: this.publisher.rollupContract,
|
|
354
|
+
log: this.log,
|
|
355
|
+
})
|
|
356
|
+
: undefined;
|
|
357
|
+
|
|
183
358
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
184
359
|
coinbase,
|
|
185
360
|
feeRecipient,
|
|
186
|
-
this.
|
|
361
|
+
this.targetSlot,
|
|
362
|
+
this.pipelinedParentSimulationOverridesPlan,
|
|
187
363
|
);
|
|
188
364
|
|
|
189
365
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
@@ -191,7 +367,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
191
367
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
192
368
|
|
|
193
369
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
194
|
-
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.
|
|
370
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch))
|
|
195
371
|
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
196
372
|
.map(c => c.checkpointOutHash);
|
|
197
373
|
|
|
@@ -224,7 +400,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
224
400
|
};
|
|
225
401
|
|
|
226
402
|
let blocksInCheckpoint: L2Block[] = [];
|
|
227
|
-
let blockPendingBroadcast:
|
|
403
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
228
404
|
const checkpointBuildTimer = new Timer();
|
|
229
405
|
|
|
230
406
|
try {
|
|
@@ -248,8 +424,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
248
424
|
}
|
|
249
425
|
|
|
250
426
|
if (blocksInCheckpoint.length === 0) {
|
|
251
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
252
|
-
this.eventEmitter.emit('checkpoint-empty', { slot: this.
|
|
427
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, { slot: this.targetSlot });
|
|
428
|
+
this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
|
|
253
429
|
return undefined;
|
|
254
430
|
}
|
|
255
431
|
|
|
@@ -257,117 +433,81 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
257
433
|
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
258
434
|
this.log.warn(
|
|
259
435
|
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
260
|
-
{ slot: this.
|
|
436
|
+
{ slot: this.targetSlot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
261
437
|
);
|
|
262
438
|
return undefined;
|
|
263
439
|
}
|
|
264
440
|
|
|
265
441
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
266
442
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
267
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
443
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
268
444
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
269
445
|
|
|
446
|
+
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
447
|
+
// Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
|
|
448
|
+
try {
|
|
449
|
+
validateCheckpoint(checkpoint, {
|
|
450
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
451
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
452
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
453
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
454
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
455
|
+
});
|
|
456
|
+
} catch (err) {
|
|
457
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
458
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
459
|
+
});
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
|
|
270
463
|
// Record checkpoint-level build metrics
|
|
271
|
-
this.
|
|
464
|
+
this.checkpointMetrics.recordCheckpointBuild(
|
|
272
465
|
checkpointBuildTimer.ms(),
|
|
273
466
|
blocksInCheckpoint.length,
|
|
274
467
|
checkpoint.getStats().txCount,
|
|
275
468
|
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
276
469
|
);
|
|
277
470
|
|
|
278
|
-
//
|
|
471
|
+
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
279
472
|
if (this.config.fishermanMode) {
|
|
280
473
|
this.log.info(
|
|
281
|
-
`Built checkpoint for slot ${this.
|
|
474
|
+
`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
|
|
282
475
|
`Skipping proposal in fisherman mode.`,
|
|
283
476
|
{
|
|
284
|
-
slot: this.
|
|
477
|
+
slot: this.targetSlot,
|
|
285
478
|
checkpoint: checkpoint.header.toInspect(),
|
|
286
479
|
blocksBuilt: blocksInCheckpoint.length,
|
|
287
480
|
},
|
|
288
481
|
);
|
|
289
482
|
this.metrics.recordCheckpointSuccess();
|
|
290
|
-
|
|
483
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
484
|
+
return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
|
|
291
485
|
}
|
|
292
486
|
|
|
293
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
294
|
-
const lastBlock = blockPendingBroadcast && {
|
|
295
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
296
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
297
|
-
txs: blockPendingBroadcast.txs,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
487
|
// Create the checkpoint proposal and broadcast it
|
|
301
488
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
302
489
|
checkpoint.header,
|
|
303
490
|
checkpoint.archive.root,
|
|
491
|
+
this.checkpointNumber,
|
|
304
492
|
feeAssetPriceModifier,
|
|
305
|
-
|
|
493
|
+
blockPendingBroadcast,
|
|
306
494
|
this.proposer,
|
|
307
495
|
checkpointProposalOptions,
|
|
308
496
|
);
|
|
309
497
|
|
|
310
498
|
const blockProposedAt = this.dateProvider.now();
|
|
311
499
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
500
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
312
501
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const blockAttestedAt = this.dateProvider.now();
|
|
316
|
-
|
|
317
|
-
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
318
|
-
|
|
319
|
-
// Proposer must sign over the attestations before pushing them to L1
|
|
320
|
-
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
321
|
-
let attestationsSignature: Signature;
|
|
322
|
-
try {
|
|
323
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
324
|
-
attestations,
|
|
325
|
-
signer,
|
|
326
|
-
this.slot,
|
|
327
|
-
this.checkpointNumber,
|
|
328
|
-
);
|
|
329
|
-
} catch (err) {
|
|
330
|
-
// We shouldn't really get here since we yield to another HA node
|
|
331
|
-
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
332
|
-
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
throw err;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Enqueue publishing the checkpoint to L1
|
|
339
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
340
|
-
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
341
|
-
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
342
|
-
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
343
|
-
|
|
344
|
-
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
345
|
-
if (
|
|
346
|
-
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
347
|
-
this.config.skipPublishingCheckpointsPercent > 0
|
|
348
|
-
) {
|
|
349
|
-
const result = Math.max(0, randomInt(100));
|
|
350
|
-
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
351
|
-
this.log.warn(
|
|
352
|
-
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
353
|
-
);
|
|
354
|
-
return checkpoint;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
359
|
-
txTimeoutAt,
|
|
360
|
-
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
return checkpoint;
|
|
502
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
503
|
+
return { checkpoint, proposal, blockProposedAt };
|
|
364
504
|
} catch (err) {
|
|
365
505
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
366
506
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
367
507
|
return undefined;
|
|
368
508
|
}
|
|
369
509
|
|
|
370
|
-
this.log.error(`Error building checkpoint at slot ${this.
|
|
510
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
371
511
|
return undefined;
|
|
372
512
|
}
|
|
373
513
|
}
|
|
@@ -383,17 +523,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
383
523
|
blockProposalOptions: BlockProposalOptions,
|
|
384
524
|
): Promise<{
|
|
385
525
|
blocksInCheckpoint: L2Block[];
|
|
386
|
-
blockPendingBroadcast:
|
|
526
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
387
527
|
}> {
|
|
388
528
|
const blocksInCheckpoint: L2Block[] = [];
|
|
389
529
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
390
530
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
391
531
|
|
|
392
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
393
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
394
|
-
|
|
395
532
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
396
|
-
let blockPendingBroadcast:
|
|
533
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
397
534
|
|
|
398
535
|
while (true) {
|
|
399
536
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
@@ -405,7 +542,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
405
542
|
|
|
406
543
|
if (!timingInfo.canStart) {
|
|
407
544
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
408
|
-
slot: this.
|
|
545
|
+
slot: this.targetSlot,
|
|
409
546
|
blocksBuilt,
|
|
410
547
|
secondsIntoSlot,
|
|
411
548
|
});
|
|
@@ -424,25 +561,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
424
561
|
blockNumber,
|
|
425
562
|
indexWithinCheckpoint,
|
|
426
563
|
txHashesAlreadyIncluded,
|
|
427
|
-
remainingBlobFields,
|
|
428
564
|
});
|
|
429
565
|
|
|
430
|
-
//
|
|
431
|
-
if (
|
|
432
|
-
// If
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
566
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
567
|
+
if ('failure' in buildResult) {
|
|
568
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
569
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
436
573
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
437
574
|
continue;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
578
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
579
|
+
if ('error' in buildResult) {
|
|
443
580
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
444
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
445
|
-
slot: this.
|
|
581
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
582
|
+
slot: this.targetSlot,
|
|
446
583
|
blocksBuilt,
|
|
447
584
|
error: buildResult.error,
|
|
448
585
|
});
|
|
@@ -450,65 +587,81 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
450
587
|
break;
|
|
451
588
|
}
|
|
452
589
|
|
|
453
|
-
const { block, usedTxs
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
458
|
-
|
|
459
|
-
// Sync the proposed block to the archiver to make it available
|
|
460
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
461
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
462
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
463
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
464
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
590
|
+
const { block, usedTxs } = buildResult;
|
|
591
|
+
this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
|
|
592
|
+
isFirstBlock: blocksBuilt === 0,
|
|
593
|
+
isLastBlock: timingInfo.isLastBlock,
|
|
465
594
|
});
|
|
466
595
|
|
|
596
|
+
blocksInCheckpoint.push(block);
|
|
467
597
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
468
598
|
|
|
469
|
-
//
|
|
599
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
600
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
601
|
+
|
|
602
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
603
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
604
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
605
|
+
// If this throws, we abort the entire checkpoint.
|
|
606
|
+
await this.syncProposedBlockToArchiver(block);
|
|
607
|
+
|
|
608
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
470
609
|
if (timingInfo.isLastBlock) {
|
|
471
|
-
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.
|
|
472
|
-
slot: this.
|
|
610
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
611
|
+
slot: this.targetSlot,
|
|
473
612
|
blockNumber,
|
|
474
613
|
blocksBuilt,
|
|
475
614
|
});
|
|
476
|
-
|
|
615
|
+
|
|
616
|
+
blockPendingBroadcast = proposal;
|
|
477
617
|
break;
|
|
478
618
|
}
|
|
479
619
|
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
if (!this.config.fishermanMode) {
|
|
483
|
-
const proposal = await this.validatorClient.createBlockProposal(
|
|
484
|
-
block.header,
|
|
485
|
-
block.indexWithinCheckpoint,
|
|
486
|
-
inHash,
|
|
487
|
-
block.archive.root,
|
|
488
|
-
usedTxs,
|
|
489
|
-
this.proposer,
|
|
490
|
-
blockProposalOptions,
|
|
491
|
-
);
|
|
492
|
-
await this.p2pClient.broadcastProposal(proposal);
|
|
493
|
-
}
|
|
620
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
621
|
+
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
494
622
|
|
|
495
623
|
// Wait until the next block's start time
|
|
496
624
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
497
625
|
}
|
|
498
626
|
|
|
499
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
500
|
-
slot: this.
|
|
627
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
628
|
+
slot: this.targetSlot,
|
|
501
629
|
blocksBuilt: blocksInCheckpoint.length,
|
|
502
630
|
});
|
|
503
631
|
|
|
504
632
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
505
633
|
}
|
|
506
634
|
|
|
635
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
|
|
636
|
+
private createBlockProposal(
|
|
637
|
+
block: L2Block,
|
|
638
|
+
inHash: Fr,
|
|
639
|
+
usedTxs: Tx[],
|
|
640
|
+
blockProposalOptions: BlockProposalOptions,
|
|
641
|
+
): Promise<BlockProposal | undefined> {
|
|
642
|
+
if (this.config.fishermanMode) {
|
|
643
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
644
|
+
return Promise.resolve(undefined);
|
|
645
|
+
}
|
|
646
|
+
return this.validatorClient.createBlockProposal(
|
|
647
|
+
block.header,
|
|
648
|
+
this.checkpointNumber,
|
|
649
|
+
block.indexWithinCheckpoint,
|
|
650
|
+
inHash,
|
|
651
|
+
block.archive.root,
|
|
652
|
+
usedTxs,
|
|
653
|
+
this.proposer,
|
|
654
|
+
blockProposalOptions,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
507
658
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
508
659
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
509
660
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
510
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.
|
|
511
|
-
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
661
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
662
|
+
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
663
|
+
slot: this.targetSlot,
|
|
664
|
+
});
|
|
512
665
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
513
666
|
}
|
|
514
667
|
|
|
@@ -523,36 +676,29 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
523
676
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
524
677
|
buildDeadline: Date | undefined;
|
|
525
678
|
txHashesAlreadyIncluded: Set<string>;
|
|
526
|
-
remainingBlobFields: number;
|
|
527
679
|
},
|
|
528
|
-
): Promise<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
indexWithinCheckpoint,
|
|
534
|
-
buildDeadline,
|
|
535
|
-
txHashesAlreadyIncluded,
|
|
536
|
-
remainingBlobFields,
|
|
537
|
-
} = opts;
|
|
680
|
+
): Promise<
|
|
681
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
682
|
+
> {
|
|
683
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
684
|
+
opts;
|
|
538
685
|
|
|
539
686
|
this.log.verbose(
|
|
540
|
-
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
687
|
+
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`,
|
|
541
688
|
{ ...checkpointBuilder.getConstantData(), ...opts },
|
|
542
689
|
);
|
|
543
690
|
|
|
544
691
|
try {
|
|
545
692
|
// Wait until we have enough txs to build the block
|
|
546
|
-
const minTxs = this.
|
|
547
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
693
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
548
694
|
if (!canStartBuilding) {
|
|
549
695
|
this.log.warn(
|
|
550
|
-
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
551
|
-
{ blockNumber, slot: this.
|
|
696
|
+
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
697
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
552
698
|
);
|
|
553
|
-
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.
|
|
699
|
+
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
554
700
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
555
|
-
return
|
|
701
|
+
return { failure: 'insufficient-txs' };
|
|
556
702
|
}
|
|
557
703
|
|
|
558
704
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
@@ -563,24 +709,31 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
563
709
|
);
|
|
564
710
|
|
|
565
711
|
this.log.debug(
|
|
566
|
-
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
567
|
-
{ slot: this.
|
|
712
|
+
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
|
|
713
|
+
{ slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
|
|
568
714
|
);
|
|
569
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
570
|
-
|
|
571
|
-
// Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
|
|
572
|
-
const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
|
|
573
|
-
const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
|
|
715
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
574
716
|
|
|
575
|
-
|
|
717
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
718
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
719
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
720
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
721
|
+
const blockBuilderOptions: BlockBuilderOptions = {
|
|
576
722
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
723
|
+
maxBlockGas:
|
|
724
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
725
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
726
|
+
: undefined,
|
|
580
727
|
deadline: buildDeadline,
|
|
728
|
+
isBuildingProposal: true,
|
|
729
|
+
minValidTxs,
|
|
730
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
731
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
581
732
|
};
|
|
582
733
|
|
|
583
|
-
// Actually build the block by executing txs
|
|
734
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
735
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
736
|
+
// updated for blocks that will be discarded.
|
|
584
737
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
585
738
|
checkpointBuilder,
|
|
586
739
|
pendingTxs,
|
|
@@ -592,22 +745,27 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
592
745
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
593
746
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
594
747
|
|
|
595
|
-
|
|
596
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
597
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
598
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
599
|
-
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
748
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
600
749
|
this.log.warn(
|
|
601
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.
|
|
602
|
-
{
|
|
750
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
|
|
751
|
+
{
|
|
752
|
+
slot: this.targetSlot,
|
|
753
|
+
blockNumber,
|
|
754
|
+
numTxs: buildResult.processedCount,
|
|
755
|
+
indexWithinCheckpoint,
|
|
756
|
+
minValidTxs,
|
|
757
|
+
},
|
|
603
758
|
);
|
|
604
|
-
this.eventEmitter.emit('block-build-failed', {
|
|
759
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
760
|
+
reason: `Insufficient valid txs`,
|
|
761
|
+
slot: this.targetSlot,
|
|
762
|
+
});
|
|
605
763
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
606
|
-
return
|
|
764
|
+
return { failure: 'insufficient-valid-txs' };
|
|
607
765
|
}
|
|
608
766
|
|
|
609
767
|
// Block creation succeeded, emit stats and metrics
|
|
610
|
-
const {
|
|
768
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
611
769
|
|
|
612
770
|
const blockStats = {
|
|
613
771
|
eventName: 'l2-block-built',
|
|
@@ -618,33 +776,42 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
618
776
|
|
|
619
777
|
const blockHash = await block.hash();
|
|
620
778
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
621
|
-
const manaPerSec =
|
|
779
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
622
780
|
|
|
623
781
|
this.log.info(
|
|
624
|
-
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
782
|
+
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`,
|
|
625
783
|
{ blockHash, txHashes, manaPerSec, ...blockStats },
|
|
626
784
|
);
|
|
627
785
|
|
|
628
|
-
|
|
629
|
-
|
|
786
|
+
// `slot` is the target/submission slot (may be one ahead when pipelining),
|
|
787
|
+
// `buildSlot` is the wall-clock slot during which the block was actually built.
|
|
788
|
+
this.eventEmitter.emit('block-proposed', {
|
|
789
|
+
blockNumber: block.number,
|
|
790
|
+
slot: this.targetSlot,
|
|
791
|
+
buildSlot: this.slotNow,
|
|
792
|
+
});
|
|
793
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
630
794
|
|
|
631
|
-
return { block, usedTxs
|
|
795
|
+
return { block, usedTxs };
|
|
632
796
|
} catch (err: any) {
|
|
633
|
-
this.eventEmitter.emit('block-build-failed', {
|
|
634
|
-
|
|
797
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
798
|
+
reason: err.message,
|
|
799
|
+
slot: this.targetSlot,
|
|
800
|
+
});
|
|
801
|
+
this.log.error(`Error building block`, err, { blockNumber, slot: this.targetSlot });
|
|
635
802
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
636
803
|
this.metrics.recordFailedBlock();
|
|
637
804
|
return { error: err };
|
|
638
805
|
}
|
|
639
806
|
}
|
|
640
807
|
|
|
641
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
808
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
642
809
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
643
810
|
checkpointBuilder: CheckpointBuilder,
|
|
644
811
|
pendingTxs: AsyncIterable<Tx>,
|
|
645
812
|
blockNumber: BlockNumber,
|
|
646
813
|
blockTimestamp: bigint,
|
|
647
|
-
blockBuilderOptions:
|
|
814
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
648
815
|
) {
|
|
649
816
|
try {
|
|
650
817
|
const workTimer = new Timer();
|
|
@@ -652,8 +819,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
652
819
|
const blockBuildDuration = workTimer.ms();
|
|
653
820
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
654
821
|
} catch (err: unknown) {
|
|
655
|
-
if (isErrorClass(err,
|
|
656
|
-
return {
|
|
822
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
823
|
+
return {
|
|
824
|
+
failedTxs: err.failedTxs,
|
|
825
|
+
processedCount: err.processedCount,
|
|
826
|
+
status: 'insufficient-valid-txs' as const,
|
|
827
|
+
};
|
|
657
828
|
}
|
|
658
829
|
throw err;
|
|
659
830
|
}
|
|
@@ -666,7 +837,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
666
837
|
blockNumber: BlockNumber;
|
|
667
838
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
668
839
|
buildDeadline: Date | undefined;
|
|
669
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
840
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
670
841
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
671
842
|
|
|
672
843
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -683,20 +854,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
683
854
|
// If we're past deadline, or we have no deadline, give up
|
|
684
855
|
const now = this.dateProvider.nowAsDate();
|
|
685
856
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
686
|
-
return { canStartBuilding: false, availableTxs
|
|
857
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
687
858
|
}
|
|
688
859
|
|
|
689
860
|
// Wait a bit before checking again
|
|
690
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
861
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
691
862
|
this.log.verbose(
|
|
692
|
-
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
693
|
-
{ blockNumber, slot: this.
|
|
863
|
+
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
|
|
864
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
694
865
|
);
|
|
695
866
|
await this.waitForTxsPollingInterval();
|
|
696
867
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
697
868
|
}
|
|
698
869
|
|
|
699
|
-
return { canStartBuilding: true, availableTxs };
|
|
870
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
700
871
|
}
|
|
701
872
|
|
|
702
873
|
/**
|
|
@@ -722,16 +893,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
722
893
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
723
894
|
}
|
|
724
895
|
|
|
725
|
-
const numberOfRequiredAttestations =
|
|
896
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
726
897
|
|
|
727
898
|
if (this.config.skipCollectingAttestations) {
|
|
728
899
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
729
|
-
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
900
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
730
901
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
731
902
|
}
|
|
732
903
|
|
|
733
904
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
734
|
-
? this.timetable.
|
|
905
|
+
? this.timetable.getCheckpointAttestationDeadline()
|
|
735
906
|
: this.l1Constants.slotDuration;
|
|
736
907
|
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
737
908
|
|
|
@@ -744,6 +915,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
744
915
|
proposal,
|
|
745
916
|
numberOfRequiredAttestations,
|
|
746
917
|
attestationDeadline,
|
|
918
|
+
this.checkpointNumber,
|
|
747
919
|
);
|
|
748
920
|
|
|
749
921
|
collectedAttestationsCount = attestations.length;
|
|
@@ -870,9 +1042,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
870
1042
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
871
1043
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
872
1044
|
* would never receive its own block without this explicit sync.
|
|
1045
|
+
*
|
|
1046
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1047
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1048
|
+
* whenever the real proposer's block arrives from L1.
|
|
873
1049
|
*/
|
|
874
1050
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
875
|
-
if (this.config.skipPushProposedBlocksToArchiver
|
|
1051
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
876
1052
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
877
1053
|
blockNumber: block.number,
|
|
878
1054
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -890,19 +1066,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
890
1066
|
private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
|
|
891
1067
|
// Perform L1 fee analysis before clearing requests
|
|
892
1068
|
// The callback is invoked asynchronously after the next block is mined
|
|
893
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1069
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, analysis =>
|
|
894
1070
|
this.metrics.recordFishermanFeeAnalysis(analysis),
|
|
895
1071
|
);
|
|
896
1072
|
|
|
897
1073
|
if (checkpoint) {
|
|
898
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1074
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
899
1075
|
...checkpoint.toCheckpointInfo(),
|
|
900
1076
|
...checkpoint.getStats(),
|
|
901
1077
|
feeAnalysisId: feeAnalysis?.id,
|
|
902
1078
|
});
|
|
903
1079
|
} else {
|
|
904
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
905
|
-
slot: this.
|
|
1080
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1081
|
+
slot: this.targetSlot,
|
|
906
1082
|
feeAnalysisId: feeAnalysis?.id,
|
|
907
1083
|
});
|
|
908
1084
|
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
@@ -916,15 +1092,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
916
1092
|
*/
|
|
917
1093
|
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
918
1094
|
if (err instanceof DutyAlreadySignedError) {
|
|
919
|
-
this.log.info(`${errorContext} for slot ${this.
|
|
920
|
-
slot: this.
|
|
1095
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
|
|
1096
|
+
slot: this.targetSlot,
|
|
921
1097
|
signedByNode: err.signedByNode,
|
|
922
1098
|
});
|
|
923
1099
|
return true;
|
|
924
1100
|
}
|
|
925
1101
|
if (err instanceof SlashingProtectionError) {
|
|
926
|
-
this.log.info(`${errorContext} for slot ${this.
|
|
927
|
-
slot: this.
|
|
1102
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
|
|
1103
|
+
slot: this.targetSlot,
|
|
928
1104
|
existingMessageHash: err.existingMessageHash,
|
|
929
1105
|
attemptedMessageHash: err.attemptedMessageHash,
|
|
930
1106
|
});
|
|
@@ -947,7 +1123,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
947
1123
|
}
|
|
948
1124
|
|
|
949
1125
|
private getSlotStartBuildTimestamp(): number {
|
|
950
|
-
return getSlotStartBuildTimestamp(this.
|
|
1126
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
951
1127
|
}
|
|
952
1128
|
|
|
953
1129
|
private getSecondsIntoSlot(): number {
|