@aztec/sequencer-client 0.0.1-commit.2f68f620 → 0.0.1-commit.321f6a9
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/README.md +27 -27
- package/dest/client/sequencer-client.d.ts +16 -3
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +17 -12
- package/dest/config.d.ts +6 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -14
- package/dest/global_variable_builder/fee_predictor.d.ts +1 -1
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -1
- package/dest/global_variable_builder/fee_predictor.js +11 -1
- package/dest/global_variable_builder/fee_provider.d.ts +1 -1
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -1
- package/dest/global_variable_builder/fee_provider.js +27 -5
- package/dest/global_variable_builder/global_builder.d.ts +3 -16
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +2 -25
- 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 +5 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +11 -1
- package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
- package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
- package/dest/publisher/l1_to_l2_messaging.js +70 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +48 -8
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +68 -1
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +2 -2
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +4 -2
- package/dest/publisher/l1_tx_failed_store/index.d.ts +2 -2
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/index.js +1 -0
- package/dest/publisher/sequencer-publisher.d.ts +34 -6
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +362 -61
- package/dest/publisher/write_json.d.ts +11 -0
- package/dest/publisher/write_json.d.ts.map +1 -0
- package/dest/publisher/write_json.js +57 -0
- package/dest/sequencer/automine/automine_factory.d.ts +5 -3
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_factory.js +2 -1
- package/dest/sequencer/automine/automine_sequencer.d.ts +43 -5
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +199 -18
- package/dest/sequencer/automine/index.d.ts +3 -0
- package/dest/sequencer/automine/index.d.ts.map +1 -0
- package/dest/sequencer/automine/index.js +2 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -16
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +98 -90
- package/dest/sequencer/events.d.ts +16 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -3
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -2
- package/dest/sequencer/requests_tracker.d.ts +22 -0
- package/dest/sequencer/requests_tracker.d.ts.map +1 -0
- package/dest/sequencer/requests_tracker.js +33 -0
- package/dest/sequencer/sequencer.d.ts +110 -36
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +329 -174
- package/dest/test/index.d.ts +3 -3
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/utils.d.ts +15 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +18 -1
- package/package.json +30 -28
- package/src/client/sequencer-client.ts +20 -13
- package/src/config.ts +25 -12
- package/src/global_variable_builder/fee_predictor.ts +11 -1
- package/src/global_variable_builder/fee_provider.ts +27 -5
- package/src/global_variable_builder/global_builder.ts +2 -34
- package/src/index.ts +1 -10
- package/src/publisher/config.ts +22 -1
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +114 -7
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +4 -3
- package/src/publisher/l1_tx_failed_store/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +383 -72
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/README.md +18 -4
- package/src/sequencer/automine/automine_factory.ts +8 -1
- package/src/sequencer/automine/automine_sequencer.ts +221 -20
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +119 -100
- package/src/sequencer/events.ts +16 -4
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +360 -187
- package/src/test/index.ts +2 -2
- package/src/test/utils.ts +33 -0
- package/dest/sequencer/chain_state_overrides.d.ts +0 -61
- package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
- package/dest/sequencer/chain_state_overrides.js +0 -98
- package/dest/sequencer/timetable.d.ts +0 -98
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -227
- package/src/sequencer/chain_state_overrides.ts +0 -162
- package/src/sequencer/timetable.ts +0 -288
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
type SlashingProposerContract,
|
|
14
14
|
buildSimulationOverridesStateOverride,
|
|
15
15
|
} from '@aztec/ethereum/contracts';
|
|
16
|
-
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
16
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer, captureWindowBlockFees } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
17
|
import {
|
|
18
18
|
type L1BlobInputs,
|
|
19
19
|
type L1TxConfig,
|
|
20
20
|
type L1TxRequest,
|
|
21
|
+
L1TxTimeoutError,
|
|
21
22
|
type L1TxUtils,
|
|
22
23
|
MAX_L1_TX_LIMIT,
|
|
23
24
|
type TransactionStats,
|
|
@@ -117,6 +118,7 @@ type L1ProcessArgs = {
|
|
|
117
118
|
export const Actions = [
|
|
118
119
|
'invalidate-by-invalid-attestation',
|
|
119
120
|
'invalidate-by-insufficient-attestations',
|
|
121
|
+
'prune',
|
|
120
122
|
'propose',
|
|
121
123
|
'governance-signal',
|
|
122
124
|
'vote-offenses',
|
|
@@ -177,6 +179,8 @@ export class SequencerPublisher {
|
|
|
177
179
|
protected log: Logger;
|
|
178
180
|
protected ethereumSlotDuration: bigint;
|
|
179
181
|
protected aztecSlotDuration: bigint;
|
|
182
|
+
private readonly previousL1BlockWaitTimeoutMs: number;
|
|
183
|
+
private readonly previousL1BlockWaitPollIntervalMs: number;
|
|
180
184
|
|
|
181
185
|
/** Date provider for wall-clock time. */
|
|
182
186
|
private readonly dateProvider: DateProvider;
|
|
@@ -205,7 +209,13 @@ export class SequencerPublisher {
|
|
|
205
209
|
protected requests: RequestWithExpiry[] = [];
|
|
206
210
|
|
|
207
211
|
constructor(
|
|
208
|
-
private config: Pick<
|
|
212
|
+
private config: Pick<
|
|
213
|
+
SequencerPublisherConfig,
|
|
214
|
+
| 'fishermanMode'
|
|
215
|
+
| 'l1TxFailedStore'
|
|
216
|
+
| 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
|
|
217
|
+
| 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
|
|
218
|
+
> &
|
|
209
219
|
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
210
220
|
deps: {
|
|
211
221
|
telemetry?: TelemetryClient;
|
|
@@ -225,6 +235,8 @@ export class SequencerPublisher {
|
|
|
225
235
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
226
236
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
227
237
|
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
238
|
+
this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
|
|
239
|
+
this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
|
|
228
240
|
this.dateProvider = deps.dateProvider;
|
|
229
241
|
this.epochCache = deps.epochCache;
|
|
230
242
|
this.lastActions = deps.lastActions;
|
|
@@ -276,25 +288,97 @@ export class SequencerPublisher {
|
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
291
|
+
* Logs the gas-pricing data of a failed L1 transaction at warn — so underpricing is diagnosable
|
|
292
|
+
* from logs even with no failed-tx store configured — and backs the record up to the store when
|
|
293
|
+
* one is. When captureFeeSummary is true, also records the fee data of the already-mined L1
|
|
294
|
+
* blocks in the target slot's inclusion window.
|
|
281
295
|
*/
|
|
282
|
-
private backupFailedTx(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
private backupFailedTx(
|
|
297
|
+
failedTx: Omit<FailedL1Tx, 'timestamp'>,
|
|
298
|
+
opts?: { captureFeeSummary?: boolean; targetSlot?: SlotNumber; sharedFeeSummary?: FailedL1Tx['gasInfo'] },
|
|
299
|
+
): void {
|
|
287
300
|
const tx: FailedL1Tx = {
|
|
288
301
|
...failedTx,
|
|
289
302
|
timestamp: Date.now(),
|
|
290
303
|
};
|
|
291
304
|
|
|
292
305
|
// Fire and forget - don't block on backup
|
|
293
|
-
void
|
|
294
|
-
|
|
295
|
-
|
|
306
|
+
void (async () => {
|
|
307
|
+
try {
|
|
308
|
+
// Prefer a pre-captured summary (shared across a batch of failures in the same slot) so we
|
|
309
|
+
// don't re-read the fee window per record. A capture error must not lose the record itself.
|
|
310
|
+
const feeSummary =
|
|
311
|
+
opts?.sharedFeeSummary ??
|
|
312
|
+
(opts?.captureFeeSummary
|
|
313
|
+
? await this.captureFeeEnvironment(opts.targetSlot).catch(() => undefined)
|
|
314
|
+
: undefined);
|
|
315
|
+
if (feeSummary) {
|
|
316
|
+
tx.gasInfo = { ...tx.gasInfo, ...feeSummary };
|
|
317
|
+
}
|
|
318
|
+
if (tx.gasInfo) {
|
|
319
|
+
this.log.warn(`Gas pricing data for failed L1 tx (${tx.failureType})`, {
|
|
320
|
+
failureType: tx.failureType,
|
|
321
|
+
actions: tx.context.actions,
|
|
322
|
+
slot: tx.context.slot,
|
|
323
|
+
...tx.gasInfo,
|
|
324
|
+
...tx.timing,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const store = await this.failedTxStore;
|
|
328
|
+
if (store) {
|
|
329
|
+
await store.saveFailedTx(tx);
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
296
332
|
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
297
|
-
}
|
|
333
|
+
}
|
|
334
|
+
})();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Captures per-block fee data for the L1 blocks in the target slot's inclusion window (the blocks the
|
|
339
|
+
* tx could have landed in) for underpricing diagnostics. Reads only already-mined blocks, so it never
|
|
340
|
+
* waits on the chain. Safe to call off the critical path: the underlying capture never throws, and this
|
|
341
|
+
* returns undefined when there is no target slot or the window is not yet mined (e.g. an early send
|
|
342
|
+
* failure), in which case the record simply carries no window data.
|
|
343
|
+
*/
|
|
344
|
+
private async captureFeeEnvironment(targetL2Slot: SlotNumber | undefined): Promise<FailedL1Tx['gasInfo']> {
|
|
345
|
+
if (targetL2Slot === undefined) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
349
|
+
// The inclusion window is [start of slot N, start of slot N+1): all L1 blocks that can include a tx
|
|
350
|
+
// for this L2 slot. getTimestampForSlot returns seconds, matching block.timestamp.
|
|
351
|
+
const windowStartS = getTimestampForSlot(targetL2Slot, l1Constants);
|
|
352
|
+
const windowEndS = getTimestampForSlot(SlotNumber(Number(targetL2Slot) + 1), l1Constants);
|
|
353
|
+
const windowBlocks = await captureWindowBlockFees(this.l1TxUtils.client, windowStartS, windowEndS);
|
|
354
|
+
if (windowBlocks.length === 0) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
return { windowBlocks };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Computes timing info relative to the L2 slot deadline. */
|
|
361
|
+
private computeTimingInfo(targetL2Slot: SlotNumber | undefined): FailedL1Tx['timing'] {
|
|
362
|
+
if (targetL2Slot === undefined) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
366
|
+
const slotDeadlineS = getTimestampForSlot(SlotNumber(Number(targetL2Slot) + 1), l1Constants);
|
|
367
|
+
const slotDeadlineMs = Number(slotDeadlineS) * 1000;
|
|
368
|
+
return {
|
|
369
|
+
targetL2Slot: Number(targetL2Slot),
|
|
370
|
+
slotDeadlineTimestampS: slotDeadlineS,
|
|
371
|
+
msUntilSlotDeadline: slotDeadlineMs - this.dateProvider.now(),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Builds an id for a synthetic failure record (send-error/timeout) that has no on-chain tx hash.
|
|
377
|
+
* Includes the failure time so each attempt — including retries of the same slot — is stored as its
|
|
378
|
+
* own record rather than overwriting the previous one.
|
|
379
|
+
*/
|
|
380
|
+
private failureRecordId(actions: string[], targetSlot: SlotNumber | undefined): Hex {
|
|
381
|
+
return keccak256(toHex(`${actions.join(',')}:${targetSlot ?? ''}:${Date.now()}`));
|
|
298
382
|
}
|
|
299
383
|
|
|
300
384
|
public getRollupContract(): RollupContract {
|
|
@@ -458,7 +542,9 @@ export class SequencerPublisher {
|
|
|
458
542
|
|
|
459
543
|
if (bundleResult.kind === 'aborted') {
|
|
460
544
|
this.logDroppedInSim(bundleResult.droppedRequests);
|
|
461
|
-
void this.backupDroppedInSim(bundleResult.droppedRequests)
|
|
545
|
+
void this.backupDroppedInSim(bundleResult.droppedRequests, currentL2Slot).catch(err =>
|
|
546
|
+
this.log.error(`Failed to backup requests dropped in simulation`, err),
|
|
547
|
+
);
|
|
462
548
|
return undefined;
|
|
463
549
|
}
|
|
464
550
|
|
|
@@ -482,7 +568,7 @@ export class SequencerPublisher {
|
|
|
482
568
|
requests: requests.map(request => request.action),
|
|
483
569
|
txConfig,
|
|
484
570
|
});
|
|
485
|
-
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
|
|
571
|
+
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig, currentL2Slot);
|
|
486
572
|
if (result === undefined) {
|
|
487
573
|
return undefined;
|
|
488
574
|
}
|
|
@@ -498,6 +584,44 @@ export class SequencerPublisher {
|
|
|
498
584
|
} catch (err) {
|
|
499
585
|
const viemError = formatViemError(err);
|
|
500
586
|
this.log.error(`Failed to publish bundled transactions`, viemError);
|
|
587
|
+
if (err instanceof TimeoutError) {
|
|
588
|
+
const timeoutState = err instanceof L1TxTimeoutError ? err.txState : undefined;
|
|
589
|
+
void (async () => {
|
|
590
|
+
// The RPC is likely degraded right after a timeout, so back up without the block number
|
|
591
|
+
// rather than leaking an unhandled rejection.
|
|
592
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber().catch(() => 0n);
|
|
593
|
+
this.backupFailedTx(
|
|
594
|
+
{
|
|
595
|
+
id: this.failureRecordId(
|
|
596
|
+
validRequests.map(r => r.action),
|
|
597
|
+
currentL2Slot,
|
|
598
|
+
),
|
|
599
|
+
failureType: 'timeout',
|
|
600
|
+
request: { to: MULTI_CALL_3_ADDRESS as Hex, data: '0x' as Hex },
|
|
601
|
+
l1BlockNumber,
|
|
602
|
+
error: { message: viemError.message, name: 'TimeoutError' },
|
|
603
|
+
context: {
|
|
604
|
+
actions: validRequests.map(r => r.action),
|
|
605
|
+
requests: validRequests
|
|
606
|
+
.filter(r => r.request.to !== null)
|
|
607
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
608
|
+
sender: this.getSenderAddress().toString(),
|
|
609
|
+
slot: Number(currentL2Slot),
|
|
610
|
+
},
|
|
611
|
+
timing: this.computeTimingInfo(currentL2Slot),
|
|
612
|
+
gasInfo: timeoutState
|
|
613
|
+
? {
|
|
614
|
+
sentGasPriceLadder: timeoutState.gasPriceHistory,
|
|
615
|
+
attempts: timeoutState.attempts,
|
|
616
|
+
gasLimit: timeoutState.gasLimit,
|
|
617
|
+
nonce: timeoutState.nonce,
|
|
618
|
+
}
|
|
619
|
+
: undefined,
|
|
620
|
+
},
|
|
621
|
+
{ captureFeeSummary: true, targetSlot: currentL2Slot },
|
|
622
|
+
);
|
|
623
|
+
})();
|
|
624
|
+
}
|
|
501
625
|
return undefined;
|
|
502
626
|
} finally {
|
|
503
627
|
try {
|
|
@@ -525,23 +649,37 @@ export class SequencerPublisher {
|
|
|
525
649
|
}
|
|
526
650
|
|
|
527
651
|
/** Backs up entries dropped by bundle simulation, one record per dropped action. */
|
|
528
|
-
private async backupDroppedInSim(dropped: DroppedRequest[]): Promise<void> {
|
|
652
|
+
private async backupDroppedInSim(dropped: DroppedRequest[], targetSlot?: SlotNumber): Promise<void> {
|
|
529
653
|
if (dropped.length === 0) {
|
|
530
654
|
return;
|
|
531
655
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
656
|
+
// Invoked as `void backupDroppedInSim(...)` on the publish path, so it must not throw.
|
|
657
|
+
try {
|
|
658
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
659
|
+
// Every dropped entry failed in the same slot against the same L1 fee conditions, so capture
|
|
660
|
+
// the fee environment once and share it rather than re-reading the window per entry.
|
|
661
|
+
const sharedFeeSummary = await this.captureFeeEnvironment(targetSlot).catch(() => undefined);
|
|
662
|
+
const timing = this.computeTimingInfo(targetSlot);
|
|
663
|
+
for (const { request: req } of dropped) {
|
|
664
|
+
this.backupFailedTx(
|
|
665
|
+
{
|
|
666
|
+
id: keccak256(req.request.data!),
|
|
667
|
+
failureType: 'simulation',
|
|
668
|
+
request: { to: req.request.to! as Hex, data: req.request.data! },
|
|
669
|
+
l1BlockNumber,
|
|
670
|
+
error: { message: 'Bundle entry dropped: action reverted in sim' },
|
|
671
|
+
context: {
|
|
672
|
+
actions: [req.action],
|
|
673
|
+
sender: this.getSenderAddress().toString(),
|
|
674
|
+
slot: targetSlot !== undefined ? Number(targetSlot) : undefined,
|
|
675
|
+
},
|
|
676
|
+
timing,
|
|
677
|
+
},
|
|
678
|
+
{ sharedFeeSummary },
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
this.log.warn(`Failed to back up dropped-in-sim entries`, err);
|
|
545
683
|
}
|
|
546
684
|
}
|
|
547
685
|
|
|
@@ -554,6 +692,7 @@ export class SequencerPublisher {
|
|
|
554
692
|
validRequests: RequestWithExpiry[],
|
|
555
693
|
txConfig: RequestWithExpiry['gasConfig'],
|
|
556
694
|
blobConfig: L1BlobInputs | undefined,
|
|
695
|
+
targetSlot?: SlotNumber,
|
|
557
696
|
) {
|
|
558
697
|
if (!txConfig?.gasLimit) {
|
|
559
698
|
throw new Error('gasLimit is required for bundled transactions');
|
|
@@ -590,11 +729,13 @@ export class SequencerPublisher {
|
|
|
590
729
|
this.log.error('Forwarder transaction reverted on-chain; not rotating publisher', err, {
|
|
591
730
|
transactionHash: err.receipt.transactionHash,
|
|
592
731
|
});
|
|
732
|
+
this.backupRevertFailure(validRequests, err, currentPublisher, targetSlot);
|
|
593
733
|
return undefined;
|
|
594
734
|
}
|
|
595
735
|
const viemError = formatViemError(err);
|
|
596
736
|
if (!this.getNextPublisher) {
|
|
597
737
|
this.log.error('Failed to publish bundled transactions', viemError);
|
|
738
|
+
this.backupSendFailure(validRequests, viemError, currentPublisher, targetSlot);
|
|
598
739
|
return undefined;
|
|
599
740
|
}
|
|
600
741
|
this.log.warn(
|
|
@@ -608,6 +749,7 @@ export class SequencerPublisher {
|
|
|
608
749
|
viemError,
|
|
609
750
|
{ triedAddresses: triedAddresses.map(a => a.toString()) },
|
|
610
751
|
);
|
|
752
|
+
this.backupSendFailure(validRequests, viemError, currentPublisher, targetSlot);
|
|
611
753
|
return undefined;
|
|
612
754
|
}
|
|
613
755
|
currentPublisher = nextPublisher;
|
|
@@ -615,36 +757,152 @@ export class SequencerPublisher {
|
|
|
615
757
|
}
|
|
616
758
|
}
|
|
617
759
|
|
|
760
|
+
/** Backs up an on-chain revert failure to the failed tx store. */
|
|
761
|
+
private backupRevertFailure(
|
|
762
|
+
requests: RequestWithExpiry[],
|
|
763
|
+
err: MulticallForwarderRevertedError,
|
|
764
|
+
publisher: L1TxUtils,
|
|
765
|
+
targetSlot?: SlotNumber,
|
|
766
|
+
): void {
|
|
767
|
+
this.backupFailedTx(
|
|
768
|
+
{
|
|
769
|
+
id: err.receipt.transactionHash,
|
|
770
|
+
failureType: 'revert',
|
|
771
|
+
request: { to: MULTI_CALL_3_ADDRESS as Hex, data: '0x' as Hex },
|
|
772
|
+
l1BlockNumber: err.receipt.blockNumber,
|
|
773
|
+
receipt: {
|
|
774
|
+
transactionHash: err.receipt.transactionHash,
|
|
775
|
+
blockNumber: err.receipt.blockNumber,
|
|
776
|
+
gasUsed: err.receipt.gasUsed,
|
|
777
|
+
status: 'reverted',
|
|
778
|
+
},
|
|
779
|
+
error: { message: err.message, name: err.name },
|
|
780
|
+
context: {
|
|
781
|
+
actions: requests.map(r => r.action),
|
|
782
|
+
requests: requests
|
|
783
|
+
.filter(r => r.request.to !== null)
|
|
784
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
785
|
+
sender: publisher.getSenderAddress().toString(),
|
|
786
|
+
slot: targetSlot !== undefined ? Number(targetSlot) : undefined,
|
|
787
|
+
},
|
|
788
|
+
gasInfo: err.txState
|
|
789
|
+
? {
|
|
790
|
+
sentGasPrice: err.txState.gasPrice,
|
|
791
|
+
gasLimit: err.txState.gasLimit,
|
|
792
|
+
nonce: err.txState.nonce,
|
|
793
|
+
}
|
|
794
|
+
: undefined,
|
|
795
|
+
timing: this.computeTimingInfo(targetSlot),
|
|
796
|
+
},
|
|
797
|
+
{ captureFeeSummary: true, targetSlot },
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** Backs up a send failure (tx never reached chain) to the failed tx store. */
|
|
802
|
+
private backupSendFailure(
|
|
803
|
+
requests: RequestWithExpiry[],
|
|
804
|
+
error: FormattedViemError | Error,
|
|
805
|
+
publisher: L1TxUtils,
|
|
806
|
+
targetSlot?: SlotNumber,
|
|
807
|
+
): void {
|
|
808
|
+
// If we can't get the block number, still back up without it.
|
|
809
|
+
void this.l1TxUtils
|
|
810
|
+
.getBlockNumber()
|
|
811
|
+
.catch(() => 0n)
|
|
812
|
+
.then(l1BlockNumber => {
|
|
813
|
+
this.backupFailedTx(
|
|
814
|
+
{
|
|
815
|
+
id: this.failureRecordId(
|
|
816
|
+
requests.map(r => r.action),
|
|
817
|
+
targetSlot,
|
|
818
|
+
),
|
|
819
|
+
failureType: 'send-error',
|
|
820
|
+
request: { to: MULTI_CALL_3_ADDRESS as Hex, data: '0x' as Hex },
|
|
821
|
+
l1BlockNumber,
|
|
822
|
+
error: {
|
|
823
|
+
message: error.message,
|
|
824
|
+
name: 'name' in error ? error.name : undefined,
|
|
825
|
+
},
|
|
826
|
+
context: {
|
|
827
|
+
actions: requests.map(r => r.action),
|
|
828
|
+
requests: requests
|
|
829
|
+
.filter(r => r.request.to !== null)
|
|
830
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
831
|
+
sender: publisher.getSenderAddress().toString(),
|
|
832
|
+
slot: targetSlot !== undefined ? Number(targetSlot) : undefined,
|
|
833
|
+
},
|
|
834
|
+
timing: this.computeTimingInfo(targetSlot),
|
|
835
|
+
},
|
|
836
|
+
{ captureFeeSummary: true, targetSlot },
|
|
837
|
+
);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
618
841
|
/*
|
|
619
842
|
* Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
|
|
620
|
-
* Sleeps until one L1 slot before the L2 slot boundary so the tx has a chance of being
|
|
621
|
-
* picked up by the first L1 block of the L2 slot.
|
|
622
|
-
* NB: there is a known correctness risk — being included in the L1 block right before the
|
|
623
|
-
* L2 slot starts would revert propose with HeaderLib__InvalidSlotNumber.
|
|
624
|
-
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
625
843
|
*/
|
|
626
844
|
public async sendRequestsAt(targetSlot: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
627
|
-
|
|
628
|
-
// Start of the target L2 slot, in ms (getTimestampForSlot returns seconds).
|
|
629
|
-
const startOfTargetSlotMs = Number(getTimestampForSlot(targetSlot, l1Constants)) * 1000;
|
|
630
|
-
// Aim to be in the mempool one L1 slot before the L2 slot starts, so we have a chance of
|
|
631
|
-
// being picked up by the first L1 block of the L2 slot.
|
|
632
|
-
const submitAfterMs = startOfTargetSlotMs - Number(this.ethereumSlotDuration) * 1000;
|
|
845
|
+
await this.waitForTargetSlot(targetSlot);
|
|
633
846
|
if (this.interrupted) {
|
|
634
847
|
return undefined;
|
|
635
848
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
849
|
+
|
|
850
|
+
return this.sendRequests(targetSlot);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
|
|
855
|
+
* to be mined, so we don't risk being included in it. If that block never gets mined after
|
|
856
|
+
* a timeout, we assume it got skipped on L1, so we send the tx anyway.
|
|
857
|
+
*/
|
|
858
|
+
private async waitForTargetSlot(targetSlot: SlotNumber): Promise<void> {
|
|
859
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
860
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
861
|
+
const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
|
|
862
|
+
const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
|
|
863
|
+
const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
|
|
864
|
+
const logCtx = { targetSlot, startOfTargetSlotTs, nowInSeconds, previousL1BlockTs, waitDeadlineTs };
|
|
865
|
+
|
|
866
|
+
// Check if we are already past time
|
|
867
|
+
if (nowInSeconds >= startOfTargetSlotTs) {
|
|
868
|
+
this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Otherwise we wait
|
|
873
|
+
this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
|
|
874
|
+
|
|
875
|
+
// Wait until previous L1 block timestamp first
|
|
876
|
+
const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
|
|
877
|
+
if (sleepMs > 0 && !this.interrupted) {
|
|
878
|
+
this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
|
|
642
879
|
await this.interruptibleSleep.sleep(sleepMs);
|
|
643
880
|
}
|
|
644
|
-
|
|
645
|
-
|
|
881
|
+
|
|
882
|
+
// Then loop until we see the previous L1 block, so we know that we cannot be included in it.
|
|
883
|
+
// We time out after a while, once we are sure that that block is skipped in L1.
|
|
884
|
+
while (!this.interrupted) {
|
|
885
|
+
try {
|
|
886
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
887
|
+
logCtx.nowInSeconds = nowInSeconds;
|
|
888
|
+
|
|
889
|
+
if (nowInSeconds >= waitDeadlineTs) {
|
|
890
|
+
this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const latestBlockTs = await this.l1TxUtils.getBlock().then(b => b.timestamp);
|
|
895
|
+
if (latestBlockTs >= previousL1BlockTs) {
|
|
896
|
+
this.log.debug(`Previous L1 block mined, proceeding to send requests`, { ...logCtx, latestBlockTs });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
this.log.trace(`Previous L1 block not mined yet, continuing to wait`, { ...logCtx, latestBlockTs });
|
|
900
|
+
} catch (err) {
|
|
901
|
+
this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
|
|
902
|
+
} finally {
|
|
903
|
+
await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
|
|
904
|
+
}
|
|
646
905
|
}
|
|
647
|
-
return this.sendRequests(targetSlot);
|
|
648
906
|
}
|
|
649
907
|
|
|
650
908
|
private callbackBundledTransactions(
|
|
@@ -841,18 +1099,22 @@ export class SequencerPublisher {
|
|
|
841
1099
|
|
|
842
1100
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
843
1101
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
844
|
-
this.backupFailedTx(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1102
|
+
this.backupFailedTx(
|
|
1103
|
+
{
|
|
1104
|
+
id: keccak256(request.data!),
|
|
1105
|
+
failureType: 'simulation',
|
|
1106
|
+
request: { to: request.to!, data: request.data!, value: request.value },
|
|
1107
|
+
l1BlockNumber,
|
|
1108
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1109
|
+
context: {
|
|
1110
|
+
actions: [`invalidate-${reason}`],
|
|
1111
|
+
checkpointNumber,
|
|
1112
|
+
sender: this.getSenderAddress().toString(),
|
|
1113
|
+
},
|
|
1114
|
+
timing: this.computeTimingInfo(this.getCurrentL2Slot()),
|
|
854
1115
|
},
|
|
855
|
-
|
|
1116
|
+
{ captureFeeSummary: true, targetSlot: this.getCurrentL2Slot() },
|
|
1117
|
+
);
|
|
856
1118
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
857
1119
|
}
|
|
858
1120
|
}
|
|
@@ -866,7 +1128,11 @@ export class SequencerPublisher {
|
|
|
866
1128
|
const logData = { ...checkpoint, reason };
|
|
867
1129
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
868
1130
|
|
|
869
|
-
|
|
1131
|
+
// Use the exact packed tuple posted to L1 verbatim. A repack via `packAttestations` is not a
|
|
1132
|
+
// byte-faithful inverse of `fromPacked` (a canonicalized yParity byte or an all-zero signature slot
|
|
1133
|
+
// round-trips differently), so it would diverge from the stored `attestationsHash` and revert the
|
|
1134
|
+
// invalidation.
|
|
1135
|
+
const attestationsAndSigners = validationResult.verbatimAttestations;
|
|
870
1136
|
|
|
871
1137
|
if (reason === 'invalid-attestation') {
|
|
872
1138
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -1015,6 +1281,47 @@ export class SequencerPublisher {
|
|
|
1015
1281
|
);
|
|
1016
1282
|
}
|
|
1017
1283
|
|
|
1284
|
+
/**
|
|
1285
|
+
* Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
|
|
1286
|
+
* `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
|
|
1287
|
+
* bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
|
|
1288
|
+
* as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
|
|
1289
|
+
* (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
|
|
1290
|
+
* chain (e.g. bad data blocking sync) can be wound back to recover.
|
|
1291
|
+
* @returns true if a prune request was enqueued, false otherwise.
|
|
1292
|
+
*/
|
|
1293
|
+
public async enqueuePruneIfPrunable(slotNumber: SlotNumber): Promise<boolean> {
|
|
1294
|
+
if (this.lastActions['prune'] === slotNumber) {
|
|
1295
|
+
this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, { slotNumber });
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
// Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
|
|
1299
|
+
// (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
|
|
1300
|
+
// and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
|
|
1301
|
+
// derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
|
|
1302
|
+
// the timestamp more granularly.
|
|
1303
|
+
const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
|
|
1304
|
+
const canPrune = await this.rollupContract.canPruneAtTime(ts).catch(err => {
|
|
1305
|
+
this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, { slotNumber });
|
|
1306
|
+
return false;
|
|
1307
|
+
});
|
|
1308
|
+
if (!canPrune) {
|
|
1309
|
+
this.log.debug(`Rollup not prunable at slot ${slotNumber}`, { slotNumber });
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
const request: L1TxRequest = {
|
|
1313
|
+
to: this.rollupContract.address,
|
|
1314
|
+
data: encodeFunctionData({ abi: RollupAbi, functionName: 'prune', args: [] }),
|
|
1315
|
+
};
|
|
1316
|
+
this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, { slotNumber });
|
|
1317
|
+
return this.enqueueRequest(
|
|
1318
|
+
'prune',
|
|
1319
|
+
request,
|
|
1320
|
+
{ address: this.rollupContract.address, abi: RollupAbi, eventName: 'PrunedPending' },
|
|
1321
|
+
slotNumber,
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1018
1325
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
1019
1326
|
public async enqueueSlashingActions(
|
|
1020
1327
|
actions: ProposerSlashAction[],
|
|
@@ -1253,18 +1560,22 @@ export class SequencerPublisher {
|
|
|
1253
1560
|
args: [blobInput],
|
|
1254
1561
|
});
|
|
1255
1562
|
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1256
|
-
this.backupFailedTx(
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1563
|
+
this.backupFailedTx(
|
|
1564
|
+
{
|
|
1565
|
+
id: keccak256(validateBlobsData),
|
|
1566
|
+
failureType: 'simulation',
|
|
1567
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1568
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1569
|
+
l1BlockNumber,
|
|
1570
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1571
|
+
context: {
|
|
1572
|
+
actions: ['validate-blobs'],
|
|
1573
|
+
sender: this.getSenderAddress().toString(),
|
|
1574
|
+
},
|
|
1575
|
+
timing: this.computeTimingInfo(this.getCurrentL2Slot()),
|
|
1266
1576
|
},
|
|
1267
|
-
|
|
1577
|
+
{ captureFeeSummary: true, targetSlot: this.getCurrentL2Slot() },
|
|
1578
|
+
);
|
|
1268
1579
|
throw new Error('Failed to validate blobs');
|
|
1269
1580
|
});
|
|
1270
1581
|
}
|