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