@aztec/sequencer-client 0.0.1-commit.35158ae7e → 0.0.1-commit.3750d92a7
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 +3 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +3 -4
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -11
- 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 +3 -8
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +4 -39
- 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 +1 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +2 -2
- 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 +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 +47 -44
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +112 -115
- 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 +21 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +200 -134
- 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/metrics.d.ts +5 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +12 -20
- package/dest/sequencer/sequencer.d.ts +17 -4
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +73 -20
- package/dest/sequencer/timetable.d.ts +14 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +45 -36
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +5 -7
- package/src/config.ts +8 -11
- 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 +8 -43
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +6 -4
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +174 -163
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +242 -154
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/metrics.ts +15 -25
- package/src/sequencer/sequencer.ts +110 -22
- package/src/sequencer/timetable.ts +57 -45
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
|
|
2
3
|
import {
|
|
3
4
|
BlockNumber,
|
|
4
5
|
CheckpointNumber,
|
|
@@ -30,8 +31,8 @@ import {
|
|
|
30
31
|
type L2BlockSource,
|
|
31
32
|
MaliciousCommitteeAttestationsAndSigners,
|
|
32
33
|
} from '@aztec/stdlib/block';
|
|
33
|
-
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
34
|
-
import { getSlotStartBuildTimestamp, getTimestampForSlot } 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';
|
|
35
36
|
import { Gas } from '@aztec/stdlib/gas';
|
|
36
37
|
import {
|
|
37
38
|
type BlockBuilderOptions,
|
|
@@ -56,6 +57,11 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
56
57
|
|
|
57
58
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
58
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';
|
|
59
65
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
60
66
|
import { SequencerInterruptedError } from './errors.js';
|
|
61
67
|
import type { SequencerEvents } from './events.js';
|
|
@@ -67,6 +73,20 @@ import { SequencerState } from './utils.js';
|
|
|
67
73
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
68
74
|
const TXS_POLLING_MS = 500;
|
|
69
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
|
+
|
|
70
90
|
/**
|
|
71
91
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
72
92
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -76,10 +96,15 @@ const TXS_POLLING_MS = 500;
|
|
|
76
96
|
export class CheckpointProposalJob implements Traceable {
|
|
77
97
|
protected readonly log: Logger;
|
|
78
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
|
+
|
|
79
105
|
constructor(
|
|
80
106
|
private readonly slotNow: SlotNumber,
|
|
81
107
|
private readonly targetSlot: SlotNumber,
|
|
82
|
-
private readonly epochNow: EpochNumber,
|
|
83
108
|
private readonly targetEpoch: EpochNumber,
|
|
84
109
|
private readonly checkpointNumber: CheckpointNumber,
|
|
85
110
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
@@ -103,10 +128,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
103
128
|
private readonly epochCache: EpochCache,
|
|
104
129
|
private readonly dateProvider: DateProvider,
|
|
105
130
|
private readonly metrics: SequencerMetrics,
|
|
131
|
+
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
106
132
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
107
133
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
108
134
|
public readonly tracer: Tracer,
|
|
109
135
|
bindings?: LoggerBindings,
|
|
136
|
+
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
110
137
|
) {
|
|
111
138
|
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
112
139
|
...bindings,
|
|
@@ -114,19 +141,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
114
141
|
});
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/** The wall-clock epoch. */
|
|
123
|
-
private get epoch(): EpochNumber {
|
|
124
|
-
return this.epochNow;
|
|
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;
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
/**
|
|
128
151
|
* Executes the checkpoint proposal job.
|
|
129
|
-
*
|
|
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.
|
|
130
156
|
*/
|
|
131
157
|
@trackSpan('CheckpointProposalJob.execute')
|
|
132
158
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
@@ -145,57 +171,136 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
145
171
|
this.log,
|
|
146
172
|
).enqueueVotes();
|
|
147
173
|
|
|
148
|
-
// Build
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
152
|
-
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();
|
|
153
177
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
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;
|
|
156
185
|
}
|
|
157
186
|
|
|
187
|
+
const { checkpoint } = broadcast;
|
|
188
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
189
|
+
|
|
158
190
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
159
191
|
if (this.config.fishermanMode) {
|
|
160
192
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
161
|
-
return;
|
|
193
|
+
return checkpoint;
|
|
162
194
|
}
|
|
163
195
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
const submissionSlotTimestamp =
|
|
167
|
-
getTimestampForSlot(this.targetSlot, this.l1Constants) - BigInt(this.l1Constants.ethereumSlotDuration);
|
|
168
|
-
this.log.info(`Waiting until submission slot ${this.targetSlot} for L1 submission`, {
|
|
169
|
-
slot: this.slot,
|
|
170
|
-
submissionSlot: this.targetSlot,
|
|
171
|
-
submissionSlotTimestamp,
|
|
172
|
-
});
|
|
173
|
-
await sleepUntil(new Date(Number(submissionSlotTimestamp) * 1000), this.dateProvider.nowAsDate());
|
|
196
|
+
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
197
|
+
this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
|
|
174
198
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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,
|
|
182
229
|
);
|
|
183
|
-
|
|
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();
|
|
184
265
|
}
|
|
185
266
|
}
|
|
267
|
+
}
|
|
186
268
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
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
|
+
});
|
|
199
304
|
}
|
|
200
305
|
|
|
201
306
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
@@ -205,8 +310,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
205
310
|
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
206
311
|
};
|
|
207
312
|
})
|
|
208
|
-
private async proposeCheckpoint(): Promise<
|
|
313
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
|
|
209
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
|
+
|
|
210
325
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
211
326
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
212
327
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
@@ -214,7 +329,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
214
329
|
// Start the checkpoint
|
|
215
330
|
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
216
331
|
this.log.info(`Starting checkpoint proposal`, {
|
|
217
|
-
buildSlot: this.
|
|
332
|
+
buildSlot: this.slotNow,
|
|
218
333
|
submissionSlot: this.targetSlot,
|
|
219
334
|
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
220
335
|
proposer: this.proposer?.toString(),
|
|
@@ -227,11 +342,24 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
227
342
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
228
343
|
}
|
|
229
344
|
|
|
230
|
-
// 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
|
+
|
|
231
358
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
232
359
|
coinbase,
|
|
233
360
|
feeRecipient,
|
|
234
361
|
this.targetSlot,
|
|
362
|
+
this.pipelinedParentSimulationOverridesPlan,
|
|
235
363
|
);
|
|
236
364
|
|
|
237
365
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
@@ -272,7 +400,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
272
400
|
};
|
|
273
401
|
|
|
274
402
|
let blocksInCheckpoint: L2Block[] = [];
|
|
275
|
-
let blockPendingBroadcast:
|
|
403
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
276
404
|
const checkpointBuildTimer = new Timer();
|
|
277
405
|
|
|
278
406
|
try {
|
|
@@ -326,21 +454,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
326
454
|
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
327
455
|
});
|
|
328
456
|
} catch (err) {
|
|
329
|
-
this.log.error(`Built an invalid checkpoint at slot ${this.
|
|
457
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
330
458
|
checkpoint: checkpoint.header.toInspect(),
|
|
331
459
|
});
|
|
332
460
|
return undefined;
|
|
333
461
|
}
|
|
334
462
|
|
|
335
463
|
// Record checkpoint-level build metrics
|
|
336
|
-
this.
|
|
464
|
+
this.checkpointMetrics.recordCheckpointBuild(
|
|
337
465
|
checkpointBuildTimer.ms(),
|
|
338
466
|
blocksInCheckpoint.length,
|
|
339
467
|
checkpoint.getStats().txCount,
|
|
340
468
|
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
341
469
|
);
|
|
342
470
|
|
|
343
|
-
//
|
|
471
|
+
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
344
472
|
if (this.config.fishermanMode) {
|
|
345
473
|
this.log.info(
|
|
346
474
|
`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
|
|
@@ -352,87 +480,34 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
352
480
|
},
|
|
353
481
|
);
|
|
354
482
|
this.metrics.recordCheckpointSuccess();
|
|
355
|
-
|
|
483
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
484
|
+
return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
|
|
356
485
|
}
|
|
357
486
|
|
|
358
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
359
|
-
const lastBlock = blockPendingBroadcast && {
|
|
360
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
361
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
362
|
-
txs: blockPendingBroadcast.txs,
|
|
363
|
-
};
|
|
364
|
-
|
|
365
487
|
// Create the checkpoint proposal and broadcast it
|
|
366
488
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
367
489
|
checkpoint.header,
|
|
368
490
|
checkpoint.archive.root,
|
|
491
|
+
this.checkpointNumber,
|
|
369
492
|
feeAssetPriceModifier,
|
|
370
|
-
|
|
493
|
+
blockPendingBroadcast,
|
|
371
494
|
this.proposer,
|
|
372
495
|
checkpointProposalOptions,
|
|
373
496
|
);
|
|
374
497
|
|
|
375
498
|
const blockProposedAt = this.dateProvider.now();
|
|
376
499
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
500
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
377
501
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const blockAttestedAt = this.dateProvider.now();
|
|
381
|
-
|
|
382
|
-
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
383
|
-
|
|
384
|
-
// Proposer must sign over the attestations before pushing them to L1
|
|
385
|
-
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
386
|
-
let attestationsSignature: Signature;
|
|
387
|
-
try {
|
|
388
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
389
|
-
attestations,
|
|
390
|
-
signer,
|
|
391
|
-
this.targetSlot,
|
|
392
|
-
this.checkpointNumber,
|
|
393
|
-
);
|
|
394
|
-
} catch (err) {
|
|
395
|
-
// We shouldn't really get here since we yield to another HA node
|
|
396
|
-
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
397
|
-
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
throw err;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Enqueue publishing the checkpoint to L1
|
|
404
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
405
|
-
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
406
|
-
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
407
|
-
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
408
|
-
|
|
409
|
-
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
410
|
-
if (
|
|
411
|
-
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
412
|
-
this.config.skipPublishingCheckpointsPercent > 0
|
|
413
|
-
) {
|
|
414
|
-
const result = Math.max(0, randomInt(100));
|
|
415
|
-
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
416
|
-
this.log.warn(
|
|
417
|
-
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
418
|
-
);
|
|
419
|
-
return checkpoint;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
424
|
-
txTimeoutAt,
|
|
425
|
-
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
return checkpoint;
|
|
502
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
503
|
+
return { checkpoint, proposal, blockProposedAt };
|
|
429
504
|
} catch (err) {
|
|
430
505
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
431
506
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
432
507
|
return undefined;
|
|
433
508
|
}
|
|
434
509
|
|
|
435
|
-
this.log.error(`Error building checkpoint at slot ${this.
|
|
510
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
436
511
|
return undefined;
|
|
437
512
|
}
|
|
438
513
|
}
|
|
@@ -448,14 +523,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
448
523
|
blockProposalOptions: BlockProposalOptions,
|
|
449
524
|
): Promise<{
|
|
450
525
|
blocksInCheckpoint: L2Block[];
|
|
451
|
-
blockPendingBroadcast:
|
|
526
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
452
527
|
}> {
|
|
453
528
|
const blocksInCheckpoint: L2Block[] = [];
|
|
454
529
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
455
530
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
456
531
|
|
|
457
532
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
458
|
-
let blockPendingBroadcast:
|
|
533
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
459
534
|
|
|
460
535
|
while (true) {
|
|
461
536
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
@@ -488,19 +563,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
488
563
|
txHashesAlreadyIncluded,
|
|
489
564
|
});
|
|
490
565
|
|
|
491
|
-
//
|
|
492
|
-
if (
|
|
493
|
-
// If
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
497
573
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
498
574
|
continue;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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) {
|
|
504
580
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
505
581
|
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
506
582
|
slot: this.targetSlot,
|
|
@@ -512,33 +588,35 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
512
588
|
}
|
|
513
589
|
|
|
514
590
|
const { block, usedTxs } = buildResult;
|
|
591
|
+
this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
|
|
592
|
+
isFirstBlock: blocksBuilt === 0,
|
|
593
|
+
isLastBlock: timingInfo.isLastBlock,
|
|
594
|
+
});
|
|
595
|
+
|
|
515
596
|
blocksInCheckpoint.push(block);
|
|
516
597
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
517
598
|
|
|
518
|
-
//
|
|
519
|
-
|
|
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.
|
|
520
609
|
if (timingInfo.isLastBlock) {
|
|
521
|
-
await this.syncProposedBlockToArchiver(block);
|
|
522
610
|
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
523
611
|
slot: this.targetSlot,
|
|
524
612
|
blockNumber,
|
|
525
613
|
blocksBuilt,
|
|
526
614
|
});
|
|
527
|
-
|
|
615
|
+
|
|
616
|
+
blockPendingBroadcast = proposal;
|
|
528
617
|
break;
|
|
529
618
|
}
|
|
530
619
|
|
|
531
|
-
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
532
|
-
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
533
|
-
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
534
|
-
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
535
|
-
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
536
|
-
|
|
537
|
-
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
538
|
-
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
539
|
-
// If this throws, we abort the entire checkpoint.
|
|
540
|
-
await this.syncProposedBlockToArchiver(block);
|
|
541
|
-
|
|
542
620
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
543
621
|
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
544
622
|
|
|
@@ -567,6 +645,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
567
645
|
}
|
|
568
646
|
return this.validatorClient.createBlockProposal(
|
|
569
647
|
block.header,
|
|
648
|
+
this.checkpointNumber,
|
|
570
649
|
block.indexWithinCheckpoint,
|
|
571
650
|
inHash,
|
|
572
651
|
block.archive.root,
|
|
@@ -598,7 +677,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
598
677
|
buildDeadline: Date | undefined;
|
|
599
678
|
txHashesAlreadyIncluded: Set<string>;
|
|
600
679
|
},
|
|
601
|
-
): Promise<
|
|
680
|
+
): Promise<
|
|
681
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
682
|
+
> {
|
|
602
683
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
603
684
|
opts;
|
|
604
685
|
|
|
@@ -617,7 +698,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
617
698
|
);
|
|
618
699
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
619
700
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
620
|
-
return
|
|
701
|
+
return { failure: 'insufficient-txs' };
|
|
621
702
|
}
|
|
622
703
|
|
|
623
704
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
@@ -680,7 +761,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
680
761
|
slot: this.targetSlot,
|
|
681
762
|
});
|
|
682
763
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
683
|
-
return
|
|
764
|
+
return { failure: 'insufficient-valid-txs' };
|
|
684
765
|
}
|
|
685
766
|
|
|
686
767
|
// Block creation succeeded, emit stats and metrics
|
|
@@ -702,12 +783,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
702
783
|
{ blockHash, txHashes, manaPerSec, ...blockStats },
|
|
703
784
|
);
|
|
704
785
|
|
|
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.
|
|
705
788
|
this.eventEmitter.emit('block-proposed', {
|
|
706
789
|
blockNumber: block.number,
|
|
707
790
|
slot: this.targetSlot,
|
|
708
791
|
buildSlot: this.slotNow,
|
|
709
792
|
});
|
|
710
|
-
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
793
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
711
794
|
|
|
712
795
|
return { block, usedTxs };
|
|
713
796
|
} catch (err: any) {
|
|
@@ -810,16 +893,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
810
893
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
811
894
|
}
|
|
812
895
|
|
|
813
|
-
const numberOfRequiredAttestations =
|
|
896
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
814
897
|
|
|
815
898
|
if (this.config.skipCollectingAttestations) {
|
|
816
899
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
817
|
-
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
900
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
818
901
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
819
902
|
}
|
|
820
903
|
|
|
821
904
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
822
|
-
? this.timetable.
|
|
905
|
+
? this.timetable.getCheckpointAttestationDeadline()
|
|
823
906
|
: this.l1Constants.slotDuration;
|
|
824
907
|
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
825
908
|
|
|
@@ -832,6 +915,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
832
915
|
proposal,
|
|
833
916
|
numberOfRequiredAttestations,
|
|
834
917
|
attestationDeadline,
|
|
918
|
+
this.checkpointNumber,
|
|
835
919
|
);
|
|
836
920
|
|
|
837
921
|
collectedAttestationsCount = attestations.length;
|
|
@@ -958,9 +1042,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
958
1042
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
959
1043
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
960
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.
|
|
961
1049
|
*/
|
|
962
1050
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
963
|
-
if (this.config.skipPushProposedBlocksToArchiver
|
|
1051
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
964
1052
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
965
1053
|
blockNumber: block.number,
|
|
966
1054
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -1035,7 +1123,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1035
1123
|
}
|
|
1036
1124
|
|
|
1037
1125
|
private getSlotStartBuildTimestamp(): number {
|
|
1038
|
-
return getSlotStartBuildTimestamp(this.
|
|
1126
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
1039
1127
|
}
|
|
1040
1128
|
|
|
1041
1129
|
private getSecondsIntoSlot(): number {
|