@aztec/sequencer-client 0.0.1-commit.f504929 → 0.0.1-commit.f650c0a5c
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 +4 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +46 -27
- package/dest/config.d.ts +25 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +21 -12
- package/dest/global_variable_builder/global_builder.d.ts +15 -9
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +29 -25
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +13 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +17 -2
- 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 +58 -32
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +132 -89
- package/dest/sequencer/checkpoint_proposal_job.d.ts +36 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +302 -180
- 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 +5 -1
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +11 -0
- package/dest/sequencer/sequencer.d.ts +23 -10
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +130 -71
- package/dest/sequencer/timetable.d.ts +7 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +21 -12
- 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 +56 -28
- package/src/config.ts +28 -14
- package/src/global_variable_builder/global_builder.ts +37 -26
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +32 -0
- package/src/publisher/sequencer-publisher-factory.ts +3 -6
- package/src/publisher/sequencer-publisher.ts +205 -127
- package/src/sequencer/README.md +81 -12
- package/src/sequencer/checkpoint_proposal_job.ts +385 -199
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +14 -0
- package/src/sequencer/sequencer.ts +186 -80
- package/src/sequencer/timetable.ts +26 -15
- 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 FeeHeader, RollupContract } 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';
|
|
@@ -64,6 +68,13 @@ import { SequencerState } from './utils.js';
|
|
|
64
68
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
65
69
|
const TXS_POLLING_MS = 500;
|
|
66
70
|
|
|
71
|
+
/** Result from proposeCheckpoint when a checkpoint was successfully built and attested. */
|
|
72
|
+
type CheckpointProposalResult = {
|
|
73
|
+
checkpoint: Checkpoint;
|
|
74
|
+
attestations: CommitteeAttestationsAndSigners;
|
|
75
|
+
attestationsSignature: Signature;
|
|
76
|
+
};
|
|
77
|
+
|
|
67
78
|
/**
|
|
68
79
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
69
80
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -73,9 +84,16 @@ const TXS_POLLING_MS = 500;
|
|
|
73
84
|
export class CheckpointProposalJob implements Traceable {
|
|
74
85
|
protected readonly log: Logger;
|
|
75
86
|
|
|
87
|
+
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
88
|
+
private pendingL1Submission: Promise<void> | undefined;
|
|
89
|
+
|
|
90
|
+
/** Fee header override computed during proposeCheckpoint, reused in enqueueCheckpointForSubmission. */
|
|
91
|
+
private computedForceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
92
|
+
|
|
76
93
|
constructor(
|
|
77
|
-
private readonly
|
|
78
|
-
private readonly
|
|
94
|
+
private readonly slotNow: SlotNumber,
|
|
95
|
+
private readonly targetSlot: SlotNumber,
|
|
96
|
+
private readonly targetEpoch: EpochNumber,
|
|
79
97
|
private readonly checkpointNumber: CheckpointNumber,
|
|
80
98
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
81
99
|
// TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
@@ -102,13 +120,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
102
120
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
103
121
|
public readonly tracer: Tracer,
|
|
104
122
|
bindings?: LoggerBindings,
|
|
123
|
+
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
105
124
|
) {
|
|
106
|
-
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
125
|
+
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
126
|
+
...bindings,
|
|
127
|
+
instanceId: `slot-${this.slotNow}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
132
|
+
public async awaitPendingSubmission(): Promise<void> {
|
|
133
|
+
this.log.info('Awaiting pending L1 payload submission');
|
|
134
|
+
await this.pendingL1Submission;
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
/**
|
|
110
138
|
* Executes the checkpoint proposal job.
|
|
111
|
-
*
|
|
139
|
+
* Builds blocks, collects attestations, enqueues requests, and schedules L1 submission as a
|
|
140
|
+
* background task so the work loop can return to IDLE immediately.
|
|
141
|
+
* Returns the built checkpoint if successful, undefined otherwise.
|
|
112
142
|
*/
|
|
113
143
|
@trackSpan('CheckpointProposalJob.execute')
|
|
114
144
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
@@ -116,7 +146,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
116
146
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
117
147
|
// These are constant for the whole slot, so we only enqueue them once
|
|
118
148
|
const votesPromises = new CheckpointVoter(
|
|
119
|
-
this.
|
|
149
|
+
this.targetSlot,
|
|
120
150
|
this.publisher,
|
|
121
151
|
this.attestorAddress,
|
|
122
152
|
this.validatorClient,
|
|
@@ -127,8 +157,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
127
157
|
this.log,
|
|
128
158
|
).enqueueVotes();
|
|
129
159
|
|
|
130
|
-
// Build and propose the checkpoint.
|
|
131
|
-
|
|
160
|
+
// Build and propose the checkpoint. Builds blocks, broadcasts, collects attestations, and signs.
|
|
161
|
+
// Does NOT enqueue to L1 yet — that happens after the pipeline sleep.
|
|
162
|
+
const proposalResult = await this.proposeCheckpoint();
|
|
163
|
+
const checkpoint = proposalResult?.checkpoint;
|
|
132
164
|
|
|
133
165
|
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
134
166
|
await Promise.all(votesPromises);
|
|
@@ -143,47 +175,133 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
143
175
|
return;
|
|
144
176
|
}
|
|
145
177
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} else if (checkpoint) {
|
|
155
|
-
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.slot });
|
|
156
|
-
return undefined;
|
|
178
|
+
// Enqueue the checkpoint for L1 submission
|
|
179
|
+
if (proposalResult) {
|
|
180
|
+
try {
|
|
181
|
+
await this.enqueueCheckpointForSubmission(proposalResult);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.log.error(`Failed to enqueue checkpoint for L1 submission at slot ${this.targetSlot}`, err);
|
|
184
|
+
// Continue to sendRequestsAt so votes are still sent
|
|
185
|
+
}
|
|
157
186
|
}
|
|
187
|
+
|
|
188
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
189
|
+
const submitAfter = this.epochCache.isProposerPipeliningEnabled()
|
|
190
|
+
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
191
|
+
: new Date(this.dateProvider.now());
|
|
192
|
+
|
|
193
|
+
// Schedule L1 submission in the background so the work loop returns immediately.
|
|
194
|
+
// The publisher will sleep until submitAfter, then send the bundled requests.
|
|
195
|
+
// The promise is stored so it can be awaited during shutdown.
|
|
196
|
+
this.pendingL1Submission = this.publisher
|
|
197
|
+
.sendRequestsAt(submitAfter)
|
|
198
|
+
.then(async l1Response => {
|
|
199
|
+
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
200
|
+
if (proposedAction) {
|
|
201
|
+
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
202
|
+
const coinbase = checkpoint?.header.coinbase;
|
|
203
|
+
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
204
|
+
} else if (checkpoint) {
|
|
205
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
206
|
+
|
|
207
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
208
|
+
this.metrics.recordPipelineDiscard();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.catch(err => {
|
|
213
|
+
this.log.error(`Background L1 submission failed for slot ${this.targetSlot}`, err);
|
|
214
|
+
if (checkpoint) {
|
|
215
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
216
|
+
|
|
217
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
218
|
+
this.metrics.recordPipelineDiscard();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
224
|
+
return checkpoint;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
|
|
228
|
+
private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
|
|
229
|
+
const { checkpoint, attestations, attestationsSignature } = result;
|
|
230
|
+
|
|
231
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
232
|
+
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
233
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
234
|
+
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
235
|
+
|
|
236
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
237
|
+
if (
|
|
238
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
239
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
240
|
+
) {
|
|
241
|
+
const roll = Math.max(0, randomInt(100));
|
|
242
|
+
if (roll < this.config.skipPublishingCheckpointsPercent) {
|
|
243
|
+
this.log.warn(
|
|
244
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`,
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
251
|
+
txTimeoutAt,
|
|
252
|
+
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
253
|
+
forceProposedFeeHeader: this.computedForceProposedFeeHeader,
|
|
254
|
+
});
|
|
158
255
|
}
|
|
159
256
|
|
|
160
257
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
161
258
|
return {
|
|
162
259
|
// nullish operator needed for tests
|
|
163
260
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
164
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
261
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
165
262
|
};
|
|
166
263
|
})
|
|
167
|
-
private async proposeCheckpoint(): Promise<
|
|
264
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalResult | undefined> {
|
|
168
265
|
try {
|
|
169
266
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
170
267
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
171
268
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
172
269
|
|
|
173
270
|
// Start the checkpoint
|
|
174
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
175
|
-
this.
|
|
271
|
+
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
272
|
+
this.log.info(`Starting checkpoint proposal`, {
|
|
273
|
+
buildSlot: this.slotNow,
|
|
274
|
+
submissionSlot: this.targetSlot,
|
|
275
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
276
|
+
proposer: this.proposer?.toString(),
|
|
277
|
+
coinbase: coinbase.toString(),
|
|
278
|
+
});
|
|
279
|
+
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
176
280
|
|
|
177
281
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
178
282
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
179
283
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
180
284
|
}
|
|
181
285
|
|
|
182
|
-
// Create checkpoint builder for the slot
|
|
286
|
+
// Create checkpoint builder for the slot.
|
|
287
|
+
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
288
|
+
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
289
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
290
|
+
const parentCheckpointNumber = isPipelining ? CheckpointNumber(this.checkpointNumber - 1) : undefined;
|
|
291
|
+
|
|
292
|
+
// Compute the parent's fee header override when pipelining
|
|
293
|
+
if (isPipelining && this.proposedCheckpointData) {
|
|
294
|
+
this.computedForceProposedFeeHeader = await this.computeForceProposedFeeHeader(parentCheckpointNumber!);
|
|
295
|
+
}
|
|
296
|
+
|
|
183
297
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
184
298
|
coinbase,
|
|
185
299
|
feeRecipient,
|
|
186
|
-
this.
|
|
300
|
+
this.targetSlot,
|
|
301
|
+
{
|
|
302
|
+
forcePendingCheckpointNumber: parentCheckpointNumber,
|
|
303
|
+
forceProposedFeeHeader: this.computedForceProposedFeeHeader,
|
|
304
|
+
},
|
|
187
305
|
);
|
|
188
306
|
|
|
189
307
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
@@ -191,7 +309,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
191
309
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
192
310
|
|
|
193
311
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
194
|
-
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.
|
|
312
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch))
|
|
195
313
|
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
196
314
|
.map(c => c.checkpointOutHash);
|
|
197
315
|
|
|
@@ -224,7 +342,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
224
342
|
};
|
|
225
343
|
|
|
226
344
|
let blocksInCheckpoint: L2Block[] = [];
|
|
227
|
-
let blockPendingBroadcast:
|
|
345
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
228
346
|
const checkpointBuildTimer = new Timer();
|
|
229
347
|
|
|
230
348
|
try {
|
|
@@ -248,8 +366,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
248
366
|
}
|
|
249
367
|
|
|
250
368
|
if (blocksInCheckpoint.length === 0) {
|
|
251
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
252
|
-
this.eventEmitter.emit('checkpoint-empty', { slot: this.
|
|
369
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, { slot: this.targetSlot });
|
|
370
|
+
this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
|
|
253
371
|
return undefined;
|
|
254
372
|
}
|
|
255
373
|
|
|
@@ -257,16 +375,33 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
257
375
|
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
258
376
|
this.log.warn(
|
|
259
377
|
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
260
|
-
{ slot: this.
|
|
378
|
+
{ slot: this.targetSlot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
261
379
|
);
|
|
262
380
|
return undefined;
|
|
263
381
|
}
|
|
264
382
|
|
|
265
383
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
266
384
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
267
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
385
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
268
386
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
269
387
|
|
|
388
|
+
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
389
|
+
// Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
|
|
390
|
+
try {
|
|
391
|
+
validateCheckpoint(checkpoint, {
|
|
392
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
393
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
394
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
395
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
396
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
397
|
+
});
|
|
398
|
+
} catch (err) {
|
|
399
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
400
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
401
|
+
});
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
270
405
|
// Record checkpoint-level build metrics
|
|
271
406
|
this.metrics.recordCheckpointBuild(
|
|
272
407
|
checkpointBuildTimer.ms(),
|
|
@@ -278,31 +413,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
278
413
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
279
414
|
if (this.config.fishermanMode) {
|
|
280
415
|
this.log.info(
|
|
281
|
-
`Built checkpoint for slot ${this.
|
|
416
|
+
`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
|
|
282
417
|
`Skipping proposal in fisherman mode.`,
|
|
283
418
|
{
|
|
284
|
-
slot: this.
|
|
419
|
+
slot: this.targetSlot,
|
|
285
420
|
checkpoint: checkpoint.header.toInspect(),
|
|
286
421
|
blocksBuilt: blocksInCheckpoint.length,
|
|
287
422
|
},
|
|
288
423
|
);
|
|
289
424
|
this.metrics.recordCheckpointSuccess();
|
|
290
|
-
return
|
|
425
|
+
return {
|
|
426
|
+
checkpoint,
|
|
427
|
+
attestations: CommitteeAttestationsAndSigners.empty(),
|
|
428
|
+
attestationsSignature: Signature.empty(),
|
|
429
|
+
};
|
|
291
430
|
}
|
|
292
431
|
|
|
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
432
|
// Create the checkpoint proposal and broadcast it
|
|
301
433
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
302
434
|
checkpoint.header,
|
|
303
435
|
checkpoint.archive.root,
|
|
304
436
|
feeAssetPriceModifier,
|
|
305
|
-
|
|
437
|
+
blockPendingBroadcast,
|
|
306
438
|
this.proposer,
|
|
307
439
|
checkpointProposalOptions,
|
|
308
440
|
);
|
|
@@ -310,7 +442,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
310
442
|
const blockProposedAt = this.dateProvider.now();
|
|
311
443
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
312
444
|
|
|
313
|
-
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.
|
|
445
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
314
446
|
const attestations = await this.waitForAttestations(proposal);
|
|
315
447
|
const blockAttestedAt = this.dateProvider.now();
|
|
316
448
|
|
|
@@ -323,7 +455,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
323
455
|
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
324
456
|
attestations,
|
|
325
457
|
signer,
|
|
326
|
-
this.
|
|
458
|
+
this.targetSlot,
|
|
327
459
|
this.checkpointNumber,
|
|
328
460
|
);
|
|
329
461
|
} catch (err) {
|
|
@@ -335,39 +467,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
335
467
|
throw err;
|
|
336
468
|
}
|
|
337
469
|
|
|
338
|
-
//
|
|
339
|
-
|
|
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;
|
|
470
|
+
// Return the result for the caller to enqueue after the pipeline sleep
|
|
471
|
+
return { checkpoint, attestations, attestationsSignature };
|
|
364
472
|
} catch (err) {
|
|
365
473
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
366
474
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
367
475
|
return undefined;
|
|
368
476
|
}
|
|
369
477
|
|
|
370
|
-
this.log.error(`Error building checkpoint at slot ${this.
|
|
478
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
371
479
|
return undefined;
|
|
372
480
|
}
|
|
373
481
|
}
|
|
@@ -383,17 +491,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
383
491
|
blockProposalOptions: BlockProposalOptions,
|
|
384
492
|
): Promise<{
|
|
385
493
|
blocksInCheckpoint: L2Block[];
|
|
386
|
-
blockPendingBroadcast:
|
|
494
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
387
495
|
}> {
|
|
388
496
|
const blocksInCheckpoint: L2Block[] = [];
|
|
389
497
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
390
498
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
391
499
|
|
|
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
500
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
396
|
-
let blockPendingBroadcast:
|
|
501
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
397
502
|
|
|
398
503
|
while (true) {
|
|
399
504
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
@@ -405,7 +510,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
405
510
|
|
|
406
511
|
if (!timingInfo.canStart) {
|
|
407
512
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
408
|
-
slot: this.
|
|
513
|
+
slot: this.targetSlot,
|
|
409
514
|
blocksBuilt,
|
|
410
515
|
secondsIntoSlot,
|
|
411
516
|
});
|
|
@@ -424,25 +529,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
424
529
|
blockNumber,
|
|
425
530
|
indexWithinCheckpoint,
|
|
426
531
|
txHashesAlreadyIncluded,
|
|
427
|
-
remainingBlobFields,
|
|
428
532
|
});
|
|
429
533
|
|
|
430
|
-
//
|
|
431
|
-
if (
|
|
432
|
-
// If
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
534
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
535
|
+
if ('failure' in buildResult) {
|
|
536
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
537
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
436
541
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
437
542
|
continue;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
546
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
547
|
+
if ('error' in buildResult) {
|
|
443
548
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
444
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
445
|
-
slot: this.
|
|
549
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
550
|
+
slot: this.targetSlot,
|
|
446
551
|
blocksBuilt,
|
|
447
552
|
error: buildResult.error,
|
|
448
553
|
});
|
|
@@ -450,65 +555,74 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
450
555
|
break;
|
|
451
556
|
}
|
|
452
557
|
|
|
453
|
-
const { block, usedTxs
|
|
558
|
+
const { block, usedTxs } = buildResult;
|
|
454
559
|
blocksInCheckpoint.push(block);
|
|
560
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
455
561
|
|
|
456
|
-
//
|
|
457
|
-
|
|
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 });
|
|
465
|
-
});
|
|
562
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
563
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
466
564
|
|
|
467
|
-
|
|
565
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
566
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
567
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
568
|
+
// If this throws, we abort the entire checkpoint.
|
|
569
|
+
await this.syncProposedBlockToArchiver(block);
|
|
468
570
|
|
|
469
|
-
// If this is the last block,
|
|
571
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
470
572
|
if (timingInfo.isLastBlock) {
|
|
471
|
-
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.
|
|
472
|
-
slot: this.
|
|
573
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
574
|
+
slot: this.targetSlot,
|
|
473
575
|
blockNumber,
|
|
474
576
|
blocksBuilt,
|
|
475
577
|
});
|
|
476
|
-
blockPendingBroadcast =
|
|
578
|
+
blockPendingBroadcast = proposal;
|
|
477
579
|
break;
|
|
478
580
|
}
|
|
479
581
|
|
|
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
|
-
}
|
|
582
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
583
|
+
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
494
584
|
|
|
495
585
|
// Wait until the next block's start time
|
|
496
586
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
497
587
|
}
|
|
498
588
|
|
|
499
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
500
|
-
slot: this.
|
|
589
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
590
|
+
slot: this.targetSlot,
|
|
501
591
|
blocksBuilt: blocksInCheckpoint.length,
|
|
502
592
|
});
|
|
503
593
|
|
|
504
594
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
505
595
|
}
|
|
506
596
|
|
|
597
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
|
|
598
|
+
private createBlockProposal(
|
|
599
|
+
block: L2Block,
|
|
600
|
+
inHash: Fr,
|
|
601
|
+
usedTxs: Tx[],
|
|
602
|
+
blockProposalOptions: BlockProposalOptions,
|
|
603
|
+
): Promise<BlockProposal | undefined> {
|
|
604
|
+
if (this.config.fishermanMode) {
|
|
605
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
606
|
+
return Promise.resolve(undefined);
|
|
607
|
+
}
|
|
608
|
+
return this.validatorClient.createBlockProposal(
|
|
609
|
+
block.header,
|
|
610
|
+
block.indexWithinCheckpoint,
|
|
611
|
+
inHash,
|
|
612
|
+
block.archive.root,
|
|
613
|
+
usedTxs,
|
|
614
|
+
this.proposer,
|
|
615
|
+
blockProposalOptions,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
507
619
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
508
620
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
509
621
|
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`, {
|
|
622
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
623
|
+
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
624
|
+
slot: this.targetSlot,
|
|
625
|
+
});
|
|
512
626
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
513
627
|
}
|
|
514
628
|
|
|
@@ -523,36 +637,29 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
523
637
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
524
638
|
buildDeadline: Date | undefined;
|
|
525
639
|
txHashesAlreadyIncluded: Set<string>;
|
|
526
|
-
remainingBlobFields: number;
|
|
527
640
|
},
|
|
528
|
-
): Promise<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
indexWithinCheckpoint,
|
|
534
|
-
buildDeadline,
|
|
535
|
-
txHashesAlreadyIncluded,
|
|
536
|
-
remainingBlobFields,
|
|
537
|
-
} = opts;
|
|
641
|
+
): Promise<
|
|
642
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
643
|
+
> {
|
|
644
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
645
|
+
opts;
|
|
538
646
|
|
|
539
647
|
this.log.verbose(
|
|
540
|
-
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
648
|
+
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`,
|
|
541
649
|
{ ...checkpointBuilder.getConstantData(), ...opts },
|
|
542
650
|
);
|
|
543
651
|
|
|
544
652
|
try {
|
|
545
653
|
// Wait until we have enough txs to build the block
|
|
546
|
-
const minTxs = this.
|
|
547
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
654
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
548
655
|
if (!canStartBuilding) {
|
|
549
656
|
this.log.warn(
|
|
550
|
-
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
551
|
-
{ blockNumber, slot: this.
|
|
657
|
+
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
658
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
552
659
|
);
|
|
553
|
-
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.
|
|
660
|
+
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
554
661
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
555
|
-
return
|
|
662
|
+
return { failure: 'insufficient-txs' };
|
|
556
663
|
}
|
|
557
664
|
|
|
558
665
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
@@ -563,24 +670,31 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
563
670
|
);
|
|
564
671
|
|
|
565
672
|
this.log.debug(
|
|
566
|
-
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
567
|
-
{ slot: this.
|
|
673
|
+
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
|
|
674
|
+
{ slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
|
|
568
675
|
);
|
|
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;
|
|
676
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
574
677
|
|
|
575
|
-
|
|
678
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
679
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
680
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
681
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
682
|
+
const blockBuilderOptions: BlockBuilderOptions = {
|
|
576
683
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
684
|
+
maxBlockGas:
|
|
685
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
686
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
687
|
+
: undefined,
|
|
580
688
|
deadline: buildDeadline,
|
|
689
|
+
isBuildingProposal: true,
|
|
690
|
+
minValidTxs,
|
|
691
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
692
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
581
693
|
};
|
|
582
694
|
|
|
583
|
-
// Actually build the block by executing txs
|
|
695
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
696
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
697
|
+
// updated for blocks that will be discarded.
|
|
584
698
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
585
699
|
checkpointBuilder,
|
|
586
700
|
pendingTxs,
|
|
@@ -592,22 +706,27 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
592
706
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
593
707
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
594
708
|
|
|
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)) {
|
|
709
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
600
710
|
this.log.warn(
|
|
601
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.
|
|
602
|
-
{
|
|
711
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
|
|
712
|
+
{
|
|
713
|
+
slot: this.targetSlot,
|
|
714
|
+
blockNumber,
|
|
715
|
+
numTxs: buildResult.processedCount,
|
|
716
|
+
indexWithinCheckpoint,
|
|
717
|
+
minValidTxs,
|
|
718
|
+
},
|
|
603
719
|
);
|
|
604
|
-
this.eventEmitter.emit('block-build-failed', {
|
|
720
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
721
|
+
reason: `Insufficient valid txs`,
|
|
722
|
+
slot: this.targetSlot,
|
|
723
|
+
});
|
|
605
724
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
606
|
-
return
|
|
725
|
+
return { failure: 'insufficient-valid-txs' };
|
|
607
726
|
}
|
|
608
727
|
|
|
609
728
|
// Block creation succeeded, emit stats and metrics
|
|
610
|
-
const {
|
|
729
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
611
730
|
|
|
612
731
|
const blockStats = {
|
|
613
732
|
eventName: 'l2-block-built',
|
|
@@ -618,33 +737,42 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
618
737
|
|
|
619
738
|
const blockHash = await block.hash();
|
|
620
739
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
621
|
-
const manaPerSec =
|
|
740
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
622
741
|
|
|
623
742
|
this.log.info(
|
|
624
|
-
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
743
|
+
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`,
|
|
625
744
|
{ blockHash, txHashes, manaPerSec, ...blockStats },
|
|
626
745
|
);
|
|
627
746
|
|
|
628
|
-
|
|
629
|
-
|
|
747
|
+
// `slot` is the target/submission slot (may be one ahead when pipelining),
|
|
748
|
+
// `buildSlot` is the wall-clock slot during which the block was actually built.
|
|
749
|
+
this.eventEmitter.emit('block-proposed', {
|
|
750
|
+
blockNumber: block.number,
|
|
751
|
+
slot: this.targetSlot,
|
|
752
|
+
buildSlot: this.slotNow,
|
|
753
|
+
});
|
|
754
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
630
755
|
|
|
631
|
-
return { block, usedTxs
|
|
756
|
+
return { block, usedTxs };
|
|
632
757
|
} catch (err: any) {
|
|
633
|
-
this.eventEmitter.emit('block-build-failed', {
|
|
634
|
-
|
|
758
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
759
|
+
reason: err.message,
|
|
760
|
+
slot: this.targetSlot,
|
|
761
|
+
});
|
|
762
|
+
this.log.error(`Error building block`, err, { blockNumber, slot: this.targetSlot });
|
|
635
763
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
636
764
|
this.metrics.recordFailedBlock();
|
|
637
765
|
return { error: err };
|
|
638
766
|
}
|
|
639
767
|
}
|
|
640
768
|
|
|
641
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
769
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
642
770
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
643
771
|
checkpointBuilder: CheckpointBuilder,
|
|
644
772
|
pendingTxs: AsyncIterable<Tx>,
|
|
645
773
|
blockNumber: BlockNumber,
|
|
646
774
|
blockTimestamp: bigint,
|
|
647
|
-
blockBuilderOptions:
|
|
775
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
648
776
|
) {
|
|
649
777
|
try {
|
|
650
778
|
const workTimer = new Timer();
|
|
@@ -652,8 +780,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
652
780
|
const blockBuildDuration = workTimer.ms();
|
|
653
781
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
654
782
|
} catch (err: unknown) {
|
|
655
|
-
if (isErrorClass(err,
|
|
656
|
-
return {
|
|
783
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
784
|
+
return {
|
|
785
|
+
failedTxs: err.failedTxs,
|
|
786
|
+
processedCount: err.processedCount,
|
|
787
|
+
status: 'insufficient-valid-txs' as const,
|
|
788
|
+
};
|
|
657
789
|
}
|
|
658
790
|
throw err;
|
|
659
791
|
}
|
|
@@ -666,7 +798,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
666
798
|
blockNumber: BlockNumber;
|
|
667
799
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
668
800
|
buildDeadline: Date | undefined;
|
|
669
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
801
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
670
802
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
671
803
|
|
|
672
804
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -683,20 +815,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
683
815
|
// If we're past deadline, or we have no deadline, give up
|
|
684
816
|
const now = this.dateProvider.nowAsDate();
|
|
685
817
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
686
|
-
return { canStartBuilding: false, availableTxs
|
|
818
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
687
819
|
}
|
|
688
820
|
|
|
689
821
|
// Wait a bit before checking again
|
|
690
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
822
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
691
823
|
this.log.verbose(
|
|
692
|
-
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
693
|
-
{ blockNumber, slot: this.
|
|
824
|
+
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
|
|
825
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
694
826
|
);
|
|
695
827
|
await this.waitForTxsPollingInterval();
|
|
696
828
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
697
829
|
}
|
|
698
830
|
|
|
699
|
-
return { canStartBuilding: true, availableTxs };
|
|
831
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
700
832
|
}
|
|
701
833
|
|
|
702
834
|
/**
|
|
@@ -722,7 +854,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
722
854
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
723
855
|
}
|
|
724
856
|
|
|
725
|
-
const numberOfRequiredAttestations =
|
|
857
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
726
858
|
|
|
727
859
|
if (this.config.skipCollectingAttestations) {
|
|
728
860
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
@@ -870,9 +1002,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
870
1002
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
871
1003
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
872
1004
|
* would never receive its own block without this explicit sync.
|
|
1005
|
+
*
|
|
1006
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1007
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1008
|
+
* whenever the real proposer's block arrives from L1.
|
|
873
1009
|
*/
|
|
874
1010
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
875
|
-
if (this.config.skipPushProposedBlocksToArchiver
|
|
1011
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
876
1012
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
877
1013
|
blockNumber: block.number,
|
|
878
1014
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -890,19 +1026,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
890
1026
|
private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
|
|
891
1027
|
// Perform L1 fee analysis before clearing requests
|
|
892
1028
|
// The callback is invoked asynchronously after the next block is mined
|
|
893
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1029
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, analysis =>
|
|
894
1030
|
this.metrics.recordFishermanFeeAnalysis(analysis),
|
|
895
1031
|
);
|
|
896
1032
|
|
|
897
1033
|
if (checkpoint) {
|
|
898
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1034
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
899
1035
|
...checkpoint.toCheckpointInfo(),
|
|
900
1036
|
...checkpoint.getStats(),
|
|
901
1037
|
feeAnalysisId: feeAnalysis?.id,
|
|
902
1038
|
});
|
|
903
1039
|
} else {
|
|
904
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
905
|
-
slot: this.
|
|
1040
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1041
|
+
slot: this.targetSlot,
|
|
906
1042
|
feeAnalysisId: feeAnalysis?.id,
|
|
907
1043
|
});
|
|
908
1044
|
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
@@ -916,15 +1052,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
916
1052
|
*/
|
|
917
1053
|
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
918
1054
|
if (err instanceof DutyAlreadySignedError) {
|
|
919
|
-
this.log.info(`${errorContext} for slot ${this.
|
|
920
|
-
slot: this.
|
|
1055
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
|
|
1056
|
+
slot: this.targetSlot,
|
|
921
1057
|
signedByNode: err.signedByNode,
|
|
922
1058
|
});
|
|
923
1059
|
return true;
|
|
924
1060
|
}
|
|
925
1061
|
if (err instanceof SlashingProtectionError) {
|
|
926
|
-
this.log.info(`${errorContext} for slot ${this.
|
|
927
|
-
slot: this.
|
|
1062
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
|
|
1063
|
+
slot: this.targetSlot,
|
|
928
1064
|
existingMessageHash: err.existingMessageHash,
|
|
929
1065
|
attemptedMessageHash: err.attemptedMessageHash,
|
|
930
1066
|
});
|
|
@@ -933,6 +1069,56 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
933
1069
|
return false;
|
|
934
1070
|
}
|
|
935
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* In times of congestion we need to simulate using the correct fee header override for the previous block
|
|
1074
|
+
* We calculate the correct fee header values.
|
|
1075
|
+
*
|
|
1076
|
+
* If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
|
|
1077
|
+
* If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
|
|
1078
|
+
* @param parentCheckpointNumber
|
|
1079
|
+
* @returns
|
|
1080
|
+
*/
|
|
1081
|
+
protected async computeForceProposedFeeHeader(parentCheckpointNumber: CheckpointNumber): Promise<
|
|
1082
|
+
| {
|
|
1083
|
+
checkpointNumber: CheckpointNumber;
|
|
1084
|
+
feeHeader: FeeHeader;
|
|
1085
|
+
}
|
|
1086
|
+
| undefined
|
|
1087
|
+
> {
|
|
1088
|
+
if (!this.proposedCheckpointData) {
|
|
1089
|
+
return undefined;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const rollup = this.publisher.rollupContract;
|
|
1093
|
+
const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
|
|
1094
|
+
try {
|
|
1095
|
+
const [grandparentCheckpoint, manaTarget] = await Promise.all([
|
|
1096
|
+
rollup.getCheckpoint(grandparentCheckpointNumber),
|
|
1097
|
+
rollup.getManaTarget(),
|
|
1098
|
+
]);
|
|
1099
|
+
|
|
1100
|
+
if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
|
|
1101
|
+
this.log.error(
|
|
1102
|
+
`Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`,
|
|
1103
|
+
);
|
|
1104
|
+
return undefined;
|
|
1105
|
+
} else {
|
|
1106
|
+
const parentFeeHeader = RollupContract.computeChildFeeHeader(
|
|
1107
|
+
grandparentCheckpoint.feeHeader,
|
|
1108
|
+
this.proposedCheckpointData.totalManaUsed,
|
|
1109
|
+
this.proposedCheckpointData.feeAssetPriceModifier,
|
|
1110
|
+
manaTarget,
|
|
1111
|
+
);
|
|
1112
|
+
return { checkpointNumber: parentCheckpointNumber, feeHeader: parentFeeHeader };
|
|
1113
|
+
}
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
this.log.error(
|
|
1116
|
+
`Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`,
|
|
1117
|
+
);
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
936
1122
|
/** Waits until a specific time within the current slot */
|
|
937
1123
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
938
1124
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -947,7 +1133,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
947
1133
|
}
|
|
948
1134
|
|
|
949
1135
|
private getSlotStartBuildTimestamp(): number {
|
|
950
|
-
return getSlotStartBuildTimestamp(this.
|
|
1136
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
951
1137
|
}
|
|
952
1138
|
|
|
953
1139
|
private getSecondsIntoSlot(): number {
|