@aztec/sequencer-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.7ffbba4
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 +23 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +99 -16
- package/dest/config.d.ts +24 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +40 -30
- package/dest/global_variable_builder/global_builder.d.ts +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +2 -2
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +26 -8
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +338 -48
- package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +211 -56
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -1
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +111 -30
- package/dest/sequencer/sequencer.d.ts +39 -18
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +96 -37
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +5 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +22 -18
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +67 -38
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +12 -11
- package/package.json +30 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +55 -41
- package/src/global_variable_builder/global_builder.ts +3 -3
- package/src/index.ts +1 -6
- package/src/publisher/config.ts +121 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +333 -60
- package/src/sequencer/checkpoint_proposal_job.ts +288 -70
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +124 -32
- package/src/sequencer/sequencer.ts +119 -39
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +4 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +120 -76
- package/src/test/utils.ts +24 -14
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -1,40 +1,53 @@
|
|
|
1
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
2
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BlockNumber,
|
|
4
|
+
CheckpointNumber,
|
|
5
|
+
EpochNumber,
|
|
6
|
+
IndexWithinCheckpoint,
|
|
7
|
+
SlotNumber,
|
|
8
|
+
} from '@aztec/foundation/branded-types';
|
|
4
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
5
15
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
18
|
import { filter } from '@aztec/foundation/iterator';
|
|
9
|
-
import type
|
|
19
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
10
20
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
11
21
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
12
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
22
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
13
23
|
import type { P2P } from '@aztec/p2p';
|
|
14
24
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
25
|
import {
|
|
16
26
|
CommitteeAttestation,
|
|
17
27
|
CommitteeAttestationsAndSigners,
|
|
18
|
-
|
|
28
|
+
L2Block,
|
|
19
29
|
type L2BlockSink,
|
|
30
|
+
type L2BlockSource,
|
|
20
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
21
32
|
} from '@aztec/stdlib/block';
|
|
22
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
23
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
24
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
import {
|
|
37
|
+
NoValidTxsError,
|
|
38
|
+
type PublicProcessorLimits,
|
|
39
|
+
type ResolvedSequencerConfig,
|
|
40
|
+
type WorldStateSynchronizer,
|
|
29
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
30
42
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
31
43
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
32
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
44
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
33
45
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
34
46
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
35
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
36
48
|
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
37
49
|
import { CheckpointBuilder, type FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
|
|
50
|
+
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
38
51
|
|
|
39
52
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
40
53
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
@@ -56,7 +69,10 @@ const TXS_POLLING_MS = 500;
|
|
|
56
69
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
57
70
|
*/
|
|
58
71
|
export class CheckpointProposalJob implements Traceable {
|
|
72
|
+
protected readonly log: Logger;
|
|
73
|
+
|
|
59
74
|
constructor(
|
|
75
|
+
private readonly epoch: EpochNumber,
|
|
60
76
|
private readonly slot: SlotNumber,
|
|
61
77
|
private readonly checkpointNumber: CheckpointNumber,
|
|
62
78
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
@@ -70,6 +86,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
70
86
|
private readonly p2pClient: P2P,
|
|
71
87
|
private readonly worldState: WorldStateSynchronizer,
|
|
72
88
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
89
|
+
private readonly l2BlockSource: L2BlockSource,
|
|
73
90
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
74
91
|
private readonly blockSink: L2BlockSink,
|
|
75
92
|
private readonly l1Constants: SequencerRollupConstants,
|
|
@@ -81,9 +98,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
81
98
|
private readonly metrics: SequencerMetrics,
|
|
82
99
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
83
100
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
84
|
-
protected readonly log: Logger,
|
|
85
101
|
public readonly tracer: Tracer,
|
|
86
|
-
|
|
102
|
+
bindings?: LoggerBindings,
|
|
103
|
+
) {
|
|
104
|
+
this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
|
|
105
|
+
}
|
|
87
106
|
|
|
88
107
|
/**
|
|
89
108
|
* Executes the checkpoint proposal job.
|
|
@@ -113,7 +132,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
113
132
|
await Promise.all(votesPromises);
|
|
114
133
|
|
|
115
134
|
if (checkpoint) {
|
|
116
|
-
this.metrics.
|
|
135
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -169,15 +188,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
169
188
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
170
189
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
171
190
|
|
|
191
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
192
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
193
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
194
|
+
.map(c => c.checkpointOutHash);
|
|
195
|
+
|
|
196
|
+
// Get the fee asset price modifier from the oracle
|
|
197
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
198
|
+
|
|
172
199
|
// Create a long-lived forked world state for the checkpoint builder
|
|
173
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
200
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
174
201
|
|
|
175
202
|
// Create checkpoint builder for the entire slot
|
|
176
203
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
177
204
|
this.checkpointNumber,
|
|
178
205
|
checkpointGlobalVariables,
|
|
206
|
+
feeAssetPriceModifier,
|
|
179
207
|
l1ToL2Messages,
|
|
208
|
+
previousCheckpointOutHashes,
|
|
180
209
|
fork,
|
|
210
|
+
this.log.getBindings(),
|
|
181
211
|
);
|
|
182
212
|
|
|
183
213
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -191,13 +221,29 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
191
221
|
broadcastInvalidCheckpointProposal: this.config.broadcastInvalidBlockProposal,
|
|
192
222
|
};
|
|
193
223
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
let blocksInCheckpoint: L2Block[] = [];
|
|
225
|
+
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
226
|
+
const checkpointBuildTimer = new Timer();
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// Main loop: build blocks for the checkpoint
|
|
230
|
+
const result = await this.buildBlocksForCheckpoint(
|
|
231
|
+
checkpointBuilder,
|
|
232
|
+
checkpointGlobalVariables.timestamp,
|
|
233
|
+
inHash,
|
|
234
|
+
blockProposalOptions,
|
|
235
|
+
);
|
|
236
|
+
blocksInCheckpoint = result.blocksInCheckpoint;
|
|
237
|
+
blockPendingBroadcast = result.blockPendingBroadcast;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
240
|
+
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
241
|
+
// which is normal for block building (may have picked different txs)
|
|
242
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
201
247
|
|
|
202
248
|
if (blocksInCheckpoint.length === 0) {
|
|
203
249
|
this.log.warn(`No blocks were built for slot ${this.slot}`, { slot: this.slot });
|
|
@@ -205,11 +251,44 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
205
251
|
return undefined;
|
|
206
252
|
}
|
|
207
253
|
|
|
254
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
255
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
256
|
+
this.log.warn(
|
|
257
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
258
|
+
{ slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
259
|
+
);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
208
263
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
209
264
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
210
265
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
211
266
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
212
267
|
|
|
268
|
+
// Final validation round for the checkpoint before we propose it, just for safety
|
|
269
|
+
try {
|
|
270
|
+
validateCheckpoint(checkpoint, {
|
|
271
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
272
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
273
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
274
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
275
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
279
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
280
|
+
});
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Record checkpoint-level build metrics
|
|
285
|
+
this.metrics.recordCheckpointBuild(
|
|
286
|
+
checkpointBuildTimer.ms(),
|
|
287
|
+
blocksInCheckpoint.length,
|
|
288
|
+
checkpoint.getStats().txCount,
|
|
289
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
290
|
+
);
|
|
291
|
+
|
|
213
292
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
214
293
|
if (this.config.fishermanMode) {
|
|
215
294
|
this.log.info(
|
|
@@ -236,6 +315,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
236
315
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
237
316
|
checkpoint.header,
|
|
238
317
|
checkpoint.archive.root,
|
|
318
|
+
feeAssetPriceModifier,
|
|
239
319
|
lastBlock,
|
|
240
320
|
this.proposer,
|
|
241
321
|
checkpointProposalOptions,
|
|
@@ -252,13 +332,43 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
252
332
|
|
|
253
333
|
// Proposer must sign over the attestations before pushing them to L1
|
|
254
334
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
255
|
-
|
|
335
|
+
let attestationsSignature: Signature;
|
|
336
|
+
try {
|
|
337
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
338
|
+
attestations,
|
|
339
|
+
signer,
|
|
340
|
+
this.slot,
|
|
341
|
+
this.checkpointNumber,
|
|
342
|
+
);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// We shouldn't really get here since we yield to another HA node
|
|
345
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
346
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
256
351
|
|
|
257
352
|
// Enqueue publishing the checkpoint to L1
|
|
258
353
|
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
259
354
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
260
355
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
261
356
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
357
|
+
|
|
358
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
359
|
+
if (
|
|
360
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
361
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
362
|
+
) {
|
|
363
|
+
const result = Math.max(0, randomInt(100));
|
|
364
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
365
|
+
this.log.warn(
|
|
366
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
367
|
+
);
|
|
368
|
+
return checkpoint;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
262
372
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
263
373
|
txTimeoutAt,
|
|
264
374
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -266,6 +376,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
266
376
|
|
|
267
377
|
return checkpoint;
|
|
268
378
|
} catch (err) {
|
|
379
|
+
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
380
|
+
// swallow this error. It's already been logged by a function deeper in the stack
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
269
384
|
this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
|
|
270
385
|
return undefined;
|
|
271
386
|
}
|
|
@@ -281,19 +396,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
281
396
|
inHash: Fr,
|
|
282
397
|
blockProposalOptions: BlockProposalOptions,
|
|
283
398
|
): Promise<{
|
|
284
|
-
blocksInCheckpoint:
|
|
285
|
-
blockPendingBroadcast: { block:
|
|
399
|
+
blocksInCheckpoint: L2Block[];
|
|
400
|
+
blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined;
|
|
286
401
|
}> {
|
|
287
|
-
const blocksInCheckpoint:
|
|
402
|
+
const blocksInCheckpoint: L2Block[] = [];
|
|
288
403
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
289
404
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
290
405
|
|
|
291
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
292
|
-
let blockPendingBroadcast: { block:
|
|
407
|
+
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
293
408
|
|
|
294
409
|
while (true) {
|
|
295
410
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
296
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
411
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
297
412
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
298
413
|
|
|
299
414
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
@@ -322,6 +437,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
322
437
|
txHashesAlreadyIncluded,
|
|
323
438
|
});
|
|
324
439
|
|
|
440
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
325
441
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
326
442
|
// If no block was produced due to not enough txs and this was the last subslot, exit
|
|
327
443
|
break;
|
|
@@ -350,7 +466,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
350
466
|
// Sync the proposed block to the archiver to make it available
|
|
351
467
|
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
352
468
|
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
353
|
-
|
|
469
|
+
// Fire and forget - don't block the critical path, but log errors
|
|
470
|
+
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
471
|
+
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
354
475
|
|
|
355
476
|
// If this is the last block, exit the loop now so we start collecting attestations
|
|
356
477
|
if (timingInfo.isLastBlock) {
|
|
@@ -400,17 +521,17 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
400
521
|
|
|
401
522
|
/** Builds a single block. Called from the main block building loop. */
|
|
402
523
|
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
403
|
-
|
|
524
|
+
protected async buildSingleBlock(
|
|
404
525
|
checkpointBuilder: CheckpointBuilder,
|
|
405
526
|
opts: {
|
|
406
527
|
forceCreate?: boolean;
|
|
407
528
|
blockTimestamp: bigint;
|
|
408
529
|
blockNumber: BlockNumber;
|
|
409
|
-
indexWithinCheckpoint:
|
|
530
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
410
531
|
buildDeadline: Date | undefined;
|
|
411
532
|
txHashesAlreadyIncluded: Set<string>;
|
|
412
533
|
},
|
|
413
|
-
): Promise<{ block:
|
|
534
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
414
535
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
415
536
|
opts;
|
|
416
537
|
|
|
@@ -436,7 +557,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
436
557
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
437
558
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
438
559
|
const pendingTxs = filter(
|
|
439
|
-
this.p2pClient.
|
|
560
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
440
561
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
441
562
|
);
|
|
442
563
|
|
|
@@ -445,52 +566,58 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
445
566
|
{ slot: this.slot, blockNumber, indexWithinCheckpoint },
|
|
446
567
|
);
|
|
447
568
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
569
|
+
|
|
570
|
+
// Per-block limits derived at startup by computeBlockLimits(), further capped
|
|
571
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
448
572
|
const blockBuilderOptions: PublicProcessorLimits = {
|
|
449
573
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
574
|
+
maxBlockGas:
|
|
575
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
576
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
577
|
+
: undefined,
|
|
453
578
|
deadline: buildDeadline,
|
|
579
|
+
isBuildingProposal: true,
|
|
454
580
|
};
|
|
455
581
|
|
|
456
582
|
// Actually build the block by executing txs
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
583
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
584
|
+
checkpointBuilder,
|
|
585
|
+
pendingTxs,
|
|
586
|
+
blockNumber,
|
|
587
|
+
blockTimestamp,
|
|
588
|
+
blockBuilderOptions,
|
|
589
|
+
);
|
|
461
590
|
|
|
462
591
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
463
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
592
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
464
593
|
|
|
465
594
|
// Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
|
|
466
595
|
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
467
596
|
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
468
|
-
|
|
597
|
+
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
598
|
+
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
469
599
|
this.log.warn(
|
|
470
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed
|
|
471
|
-
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
|
|
600
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
601
|
+
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
|
|
472
602
|
);
|
|
473
|
-
this.eventEmitter.emit('block-
|
|
474
|
-
minTxs: minValidTxs,
|
|
475
|
-
availableTxs: numTxs,
|
|
476
|
-
slot: this.slot,
|
|
477
|
-
});
|
|
603
|
+
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
478
604
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
479
605
|
return undefined;
|
|
480
606
|
}
|
|
481
607
|
|
|
482
608
|
// Block creation succeeded, emit stats and metrics
|
|
609
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration } = buildResult;
|
|
610
|
+
|
|
483
611
|
const blockStats = {
|
|
484
612
|
eventName: 'l2-block-built',
|
|
485
613
|
duration: blockBuildDuration,
|
|
486
614
|
publicProcessDuration: publicProcessorDuration,
|
|
487
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
488
615
|
...block.getStats(),
|
|
489
616
|
} satisfies L2BlockBuiltStats;
|
|
490
617
|
|
|
491
618
|
const blockHash = await block.hash();
|
|
492
619
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
493
|
-
const manaPerSec =
|
|
620
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
494
621
|
|
|
495
622
|
this.log.info(
|
|
496
623
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -498,7 +625,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
498
625
|
);
|
|
499
626
|
|
|
500
627
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
501
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
628
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
502
629
|
|
|
503
630
|
return { block, usedTxs };
|
|
504
631
|
} catch (err: any) {
|
|
@@ -510,17 +637,40 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
510
637
|
}
|
|
511
638
|
}
|
|
512
639
|
|
|
640
|
+
/** Uses the checkpoint builder to build a block, catching specific txs */
|
|
641
|
+
private async buildSingleBlockWithCheckpointBuilder(
|
|
642
|
+
checkpointBuilder: CheckpointBuilder,
|
|
643
|
+
pendingTxs: AsyncIterable<Tx>,
|
|
644
|
+
blockNumber: BlockNumber,
|
|
645
|
+
blockTimestamp: bigint,
|
|
646
|
+
blockBuilderOptions: PublicProcessorLimits,
|
|
647
|
+
) {
|
|
648
|
+
try {
|
|
649
|
+
const workTimer = new Timer();
|
|
650
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
651
|
+
const blockBuildDuration = workTimer.ms();
|
|
652
|
+
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
653
|
+
} catch (err: unknown) {
|
|
654
|
+
if (isErrorClass(err, NoValidTxsError)) {
|
|
655
|
+
return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
|
|
656
|
+
}
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
513
661
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
514
662
|
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
515
663
|
private async waitForMinTxs(opts: {
|
|
516
664
|
forceCreate?: boolean;
|
|
517
665
|
blockNumber: BlockNumber;
|
|
518
|
-
indexWithinCheckpoint:
|
|
666
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
519
667
|
buildDeadline: Date | undefined;
|
|
520
668
|
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
521
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
522
669
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
523
670
|
|
|
671
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
672
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
673
|
+
|
|
524
674
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
525
675
|
const startBuildingDeadline = buildDeadline
|
|
526
676
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -541,7 +691,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
541
691
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
542
692
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
543
693
|
);
|
|
544
|
-
await
|
|
694
|
+
await this.waitForTxsPollingInterval();
|
|
545
695
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
546
696
|
}
|
|
547
697
|
|
|
@@ -582,7 +732,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
582
732
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
583
733
|
? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
|
|
584
734
|
: this.l1Constants.slotDuration;
|
|
585
|
-
const attestationDeadline = new Date(this.
|
|
735
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
586
736
|
|
|
587
737
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
588
738
|
|
|
@@ -597,11 +747,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
597
747
|
|
|
598
748
|
collectedAttestationsCount = attestations.length;
|
|
599
749
|
|
|
750
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
751
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
752
|
+
const trimmed = trimAttestations(
|
|
753
|
+
attestations,
|
|
754
|
+
numberOfRequiredAttestations,
|
|
755
|
+
this.attestorAddress,
|
|
756
|
+
localAddresses,
|
|
757
|
+
);
|
|
758
|
+
if (trimmed.length < attestations.length) {
|
|
759
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
760
|
+
}
|
|
761
|
+
|
|
600
762
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
601
|
-
const sorted = orderAttestations(
|
|
763
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
602
764
|
|
|
603
765
|
// Manipulate the attestations if we've been configured to do so
|
|
604
|
-
if (
|
|
766
|
+
if (
|
|
767
|
+
this.config.injectFakeAttestation ||
|
|
768
|
+
this.config.injectHighSValueAttestation ||
|
|
769
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
770
|
+
this.config.shuffleAttestationOrdering
|
|
771
|
+
) {
|
|
605
772
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
606
773
|
}
|
|
607
774
|
|
|
@@ -630,7 +797,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
630
797
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
631
798
|
);
|
|
632
799
|
|
|
633
|
-
if (
|
|
800
|
+
if (
|
|
801
|
+
this.config.injectFakeAttestation ||
|
|
802
|
+
this.config.injectHighSValueAttestation ||
|
|
803
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
804
|
+
) {
|
|
634
805
|
// Find non-empty attestations that are not from the proposer
|
|
635
806
|
const nonProposerIndices: number[] = [];
|
|
636
807
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -640,8 +811,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
640
811
|
}
|
|
641
812
|
if (nonProposerIndices.length > 0) {
|
|
642
813
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
643
|
-
this.
|
|
644
|
-
|
|
814
|
+
if (this.config.injectHighSValueAttestation) {
|
|
815
|
+
this.log.warn(
|
|
816
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
817
|
+
);
|
|
818
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
819
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
820
|
+
this.log.warn(
|
|
821
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
822
|
+
);
|
|
823
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
824
|
+
} else {
|
|
825
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
826
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
827
|
+
}
|
|
645
828
|
}
|
|
646
829
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
647
830
|
}
|
|
@@ -650,11 +833,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
650
833
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
651
834
|
|
|
652
835
|
const shuffled = [...attestations];
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
836
|
+
|
|
837
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
838
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
839
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
840
|
+
const swappable: number[] = [];
|
|
841
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
842
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
843
|
+
swappable.push(k);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (swappable.length >= 2) {
|
|
847
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
848
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
849
|
+
}
|
|
658
850
|
|
|
659
851
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
660
852
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -670,7 +862,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
670
862
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
671
863
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
672
864
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
673
|
-
await this.p2pClient.
|
|
865
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
674
866
|
}
|
|
675
867
|
|
|
676
868
|
/**
|
|
@@ -678,8 +870,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
678
870
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
679
871
|
* would never receive its own block without this explicit sync.
|
|
680
872
|
*/
|
|
681
|
-
private async syncProposedBlockToArchiver(block:
|
|
682
|
-
// TODO(palla/mbps): Change default to false once block sync is stable.
|
|
873
|
+
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
683
874
|
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
684
875
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
685
876
|
blockNumber: block.number,
|
|
@@ -713,12 +904,34 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
713
904
|
slot: this.slot,
|
|
714
905
|
feeAnalysisId: feeAnalysis?.id,
|
|
715
906
|
});
|
|
716
|
-
this.metrics.
|
|
907
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
717
908
|
}
|
|
718
909
|
|
|
719
910
|
this.publisher.clearPendingRequests();
|
|
720
911
|
}
|
|
721
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
915
|
+
*/
|
|
916
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
917
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
918
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
919
|
+
slot: this.slot,
|
|
920
|
+
signedByNode: err.signedByNode,
|
|
921
|
+
});
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
if (err instanceof SlashingProtectionError) {
|
|
925
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
926
|
+
slot: this.slot,
|
|
927
|
+
existingMessageHash: err.existingMessageHash,
|
|
928
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
929
|
+
});
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
|
|
722
935
|
/** Waits until a specific time within the current slot */
|
|
723
936
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
724
937
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -727,6 +940,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
727
940
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
728
941
|
}
|
|
729
942
|
|
|
943
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
944
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
945
|
+
await sleep(TXS_POLLING_MS);
|
|
946
|
+
}
|
|
947
|
+
|
|
730
948
|
private getSlotStartBuildTimestamp(): number {
|
|
731
949
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
732
950
|
}
|