@aztec/sequencer-client 0.0.1-commit.993d240 → 0.0.1-commit.9a89641
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 +40 -41
- 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 +18 -14
- package/dest/config.d.ts +9 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +29 -15
- 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 +39 -10
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +403 -75
- 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 +209 -19
- 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 +28 -15
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +158 -117
- package/dest/sequencer/events.d.ts +16 -4
- 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/missing_committee.d.ts +72 -0
- package/dest/sequencer/missing_committee.d.ts.map +1 -0
- package/dest/sequencer/missing_committee.js +139 -0
- 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 +113 -28
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +352 -162
- 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 +29 -28
- package/src/client/sequencer-client.ts +20 -14
- package/src/config.ts +32 -16
- 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 +430 -86
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/README.md +16 -6
- package/src/sequencer/automine/README.md +18 -4
- package/src/sequencer/automine/automine_factory.ts +8 -1
- package/src/sequencer/automine/automine_sequencer.ts +229 -21
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +177 -132
- package/src/sequencer/events.ts +16 -3
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/missing_committee.ts +192 -0
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +382 -176
- 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 -101
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -231
- package/src/sequencer/chain_state_overrides.ts +0 -162
- package/src/sequencer/timetable.ts +0 -295
|
@@ -8,16 +8,18 @@ import {
|
|
|
8
8
|
MULTI_CALL_3_ADDRESS,
|
|
9
9
|
Multicall3,
|
|
10
10
|
MulticallForwarderRevertedError,
|
|
11
|
+
type PayloadProposalStatus,
|
|
11
12
|
type RollupContract,
|
|
12
13
|
type SimulationOverridesPlan,
|
|
13
14
|
type SlashingProposerContract,
|
|
14
15
|
buildSimulationOverridesStateOverride,
|
|
15
16
|
} from '@aztec/ethereum/contracts';
|
|
16
|
-
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer, captureWindowBlockFees } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
18
|
import {
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
L1TxTimeoutError,
|
|
21
23
|
type L1TxUtils,
|
|
22
24
|
MAX_L1_TX_LIMIT,
|
|
23
25
|
type TransactionStats,
|
|
@@ -45,6 +47,7 @@ import { EmpireBaseAbi, ErrorsAbi, RollupAbi, SlashingProposerAbi } from '@aztec
|
|
|
45
47
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
46
48
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
47
49
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
50
|
+
import type { SequencerConfig } from '@aztec/stdlib/config';
|
|
48
51
|
import {
|
|
49
52
|
getLastL1SlotTimestampForL2Slot,
|
|
50
53
|
getNextL1SlotTimestamp,
|
|
@@ -117,6 +120,7 @@ type L1ProcessArgs = {
|
|
|
117
120
|
export const Actions = [
|
|
118
121
|
'invalidate-by-invalid-attestation',
|
|
119
122
|
'invalidate-by-insufficient-attestations',
|
|
123
|
+
'prune',
|
|
120
124
|
'propose',
|
|
121
125
|
'governance-signal',
|
|
122
126
|
'vote-offenses',
|
|
@@ -177,6 +181,8 @@ export class SequencerPublisher {
|
|
|
177
181
|
protected log: Logger;
|
|
178
182
|
protected ethereumSlotDuration: bigint;
|
|
179
183
|
protected aztecSlotDuration: bigint;
|
|
184
|
+
private readonly previousL1BlockWaitTimeoutMs: number;
|
|
185
|
+
private readonly previousL1BlockWaitPollIntervalMs: number;
|
|
180
186
|
|
|
181
187
|
/** Date provider for wall-clock time. */
|
|
182
188
|
private readonly dateProvider: DateProvider;
|
|
@@ -205,7 +211,14 @@ export class SequencerPublisher {
|
|
|
205
211
|
protected requests: RequestWithExpiry[] = [];
|
|
206
212
|
|
|
207
213
|
constructor(
|
|
208
|
-
private config: Pick<
|
|
214
|
+
private config: Pick<
|
|
215
|
+
SequencerPublisherConfig,
|
|
216
|
+
| 'fishermanMode'
|
|
217
|
+
| 'l1TxFailedStore'
|
|
218
|
+
| 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
|
|
219
|
+
| 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
|
|
220
|
+
> &
|
|
221
|
+
Pick<SequencerConfig, 'governanceProposerForcePayloadVote'> &
|
|
209
222
|
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
210
223
|
deps: {
|
|
211
224
|
telemetry?: TelemetryClient;
|
|
@@ -225,6 +238,8 @@ export class SequencerPublisher {
|
|
|
225
238
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
226
239
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
227
240
|
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
241
|
+
this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
|
|
242
|
+
this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
|
|
228
243
|
this.dateProvider = deps.dateProvider;
|
|
229
244
|
this.epochCache = deps.epochCache;
|
|
230
245
|
this.lastActions = deps.lastActions;
|
|
@@ -276,25 +291,97 @@ export class SequencerPublisher {
|
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
294
|
+
* Logs the gas-pricing data of a failed L1 transaction at warn — so underpricing is diagnosable
|
|
295
|
+
* from logs even with no failed-tx store configured — and backs the record up to the store when
|
|
296
|
+
* one is. When captureFeeSummary is true, also records the fee data of the already-mined L1
|
|
297
|
+
* blocks in the target slot's inclusion window.
|
|
281
298
|
*/
|
|
282
|
-
private backupFailedTx(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
299
|
+
private backupFailedTx(
|
|
300
|
+
failedTx: Omit<FailedL1Tx, 'timestamp'>,
|
|
301
|
+
opts?: { captureFeeSummary?: boolean; targetSlot?: SlotNumber; sharedFeeSummary?: FailedL1Tx['gasInfo'] },
|
|
302
|
+
): void {
|
|
287
303
|
const tx: FailedL1Tx = {
|
|
288
304
|
...failedTx,
|
|
289
305
|
timestamp: Date.now(),
|
|
290
306
|
};
|
|
291
307
|
|
|
292
308
|
// Fire and forget - don't block on backup
|
|
293
|
-
void
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
void (async () => {
|
|
310
|
+
try {
|
|
311
|
+
// Prefer a pre-captured summary (shared across a batch of failures in the same slot) so we
|
|
312
|
+
// don't re-read the fee window per record. A capture error must not lose the record itself.
|
|
313
|
+
const feeSummary =
|
|
314
|
+
opts?.sharedFeeSummary ??
|
|
315
|
+
(opts?.captureFeeSummary
|
|
316
|
+
? await this.captureFeeEnvironment(opts.targetSlot).catch(() => undefined)
|
|
317
|
+
: undefined);
|
|
318
|
+
if (feeSummary) {
|
|
319
|
+
tx.gasInfo = { ...tx.gasInfo, ...feeSummary };
|
|
320
|
+
}
|
|
321
|
+
if (tx.gasInfo) {
|
|
322
|
+
this.log.warn(`Gas pricing data for failed L1 tx (${tx.failureType})`, {
|
|
323
|
+
failureType: tx.failureType,
|
|
324
|
+
actions: tx.context.actions,
|
|
325
|
+
slot: tx.context.slot,
|
|
326
|
+
...tx.gasInfo,
|
|
327
|
+
...tx.timing,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const store = await this.failedTxStore;
|
|
331
|
+
if (store) {
|
|
332
|
+
await store.saveFailedTx(tx);
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
296
335
|
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
297
|
-
}
|
|
336
|
+
}
|
|
337
|
+
})();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Captures per-block fee data for the L1 blocks in the target slot's inclusion window (the blocks the
|
|
342
|
+
* tx could have landed in) for underpricing diagnostics. Reads only already-mined blocks, so it never
|
|
343
|
+
* waits on the chain. Safe to call off the critical path: the underlying capture never throws, and this
|
|
344
|
+
* returns undefined when there is no target slot or the window is not yet mined (e.g. an early send
|
|
345
|
+
* failure), in which case the record simply carries no window data.
|
|
346
|
+
*/
|
|
347
|
+
private async captureFeeEnvironment(targetL2Slot: SlotNumber | undefined): Promise<FailedL1Tx['gasInfo']> {
|
|
348
|
+
if (targetL2Slot === undefined) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
352
|
+
// The inclusion window is [start of slot N, start of slot N+1): all L1 blocks that can include a tx
|
|
353
|
+
// for this L2 slot. getTimestampForSlot returns seconds, matching block.timestamp.
|
|
354
|
+
const windowStartS = getTimestampForSlot(targetL2Slot, l1Constants);
|
|
355
|
+
const windowEndS = getTimestampForSlot(SlotNumber(Number(targetL2Slot) + 1), l1Constants);
|
|
356
|
+
const windowBlocks = await captureWindowBlockFees(this.l1TxUtils.client, windowStartS, windowEndS);
|
|
357
|
+
if (windowBlocks.length === 0) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return { windowBlocks };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Computes timing info relative to the L2 slot deadline. */
|
|
364
|
+
private computeTimingInfo(targetL2Slot: SlotNumber | undefined): FailedL1Tx['timing'] {
|
|
365
|
+
if (targetL2Slot === undefined) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
369
|
+
const slotDeadlineS = getTimestampForSlot(SlotNumber(Number(targetL2Slot) + 1), l1Constants);
|
|
370
|
+
const slotDeadlineMs = Number(slotDeadlineS) * 1000;
|
|
371
|
+
return {
|
|
372
|
+
targetL2Slot: Number(targetL2Slot),
|
|
373
|
+
slotDeadlineTimestampS: slotDeadlineS,
|
|
374
|
+
msUntilSlotDeadline: slotDeadlineMs - this.dateProvider.now(),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Builds an id for a synthetic failure record (send-error/timeout) that has no on-chain tx hash.
|
|
380
|
+
* Includes the failure time so each attempt — including retries of the same slot — is stored as its
|
|
381
|
+
* own record rather than overwriting the previous one.
|
|
382
|
+
*/
|
|
383
|
+
private failureRecordId(actions: string[], targetSlot: SlotNumber | undefined): Hex {
|
|
384
|
+
return keccak256(toHex(`${actions.join(',')}:${targetSlot ?? ''}:${Date.now()}`));
|
|
298
385
|
}
|
|
299
386
|
|
|
300
387
|
public getRollupContract(): RollupContract {
|
|
@@ -401,10 +488,10 @@ export class SequencerPublisher {
|
|
|
401
488
|
|
|
402
489
|
/**
|
|
403
490
|
* Sends all requests that are still valid.
|
|
404
|
-
* @param targetSlot - The target L2 slot for this send. When provided (
|
|
405
|
-
* sendRequestsAt), it is threaded into bundleSimulate so the block.timestamp override
|
|
406
|
-
*
|
|
407
|
-
*
|
|
491
|
+
* @param targetSlot - The target L2 slot for this send. When provided (the production path, via
|
|
492
|
+
* sendRequestsAt), it is threaded into bundleSimulate so the block.timestamp override matches
|
|
493
|
+
* the slot the propose is built for. When omitted, falls back to getCurrentL2Slot() for the
|
|
494
|
+
* AutomineSequencer, which publishes synchronously within the current slot.
|
|
408
495
|
* @returns one of:
|
|
409
496
|
* - A receipt and stats if the tx succeeded
|
|
410
497
|
* - a receipt and errorMsg if it failed on L1
|
|
@@ -458,7 +545,9 @@ export class SequencerPublisher {
|
|
|
458
545
|
|
|
459
546
|
if (bundleResult.kind === 'aborted') {
|
|
460
547
|
this.logDroppedInSim(bundleResult.droppedRequests);
|
|
461
|
-
void this.backupDroppedInSim(bundleResult.droppedRequests)
|
|
548
|
+
void this.backupDroppedInSim(bundleResult.droppedRequests, currentL2Slot).catch(err =>
|
|
549
|
+
this.log.error(`Failed to backup requests dropped in simulation`, err),
|
|
550
|
+
);
|
|
462
551
|
return undefined;
|
|
463
552
|
}
|
|
464
553
|
|
|
@@ -482,7 +571,7 @@ export class SequencerPublisher {
|
|
|
482
571
|
requests: requests.map(request => request.action),
|
|
483
572
|
txConfig,
|
|
484
573
|
});
|
|
485
|
-
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
|
|
574
|
+
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig, currentL2Slot);
|
|
486
575
|
if (result === undefined) {
|
|
487
576
|
return undefined;
|
|
488
577
|
}
|
|
@@ -498,6 +587,44 @@ export class SequencerPublisher {
|
|
|
498
587
|
} catch (err) {
|
|
499
588
|
const viemError = formatViemError(err);
|
|
500
589
|
this.log.error(`Failed to publish bundled transactions`, viemError);
|
|
590
|
+
if (err instanceof TimeoutError) {
|
|
591
|
+
const timeoutState = err instanceof L1TxTimeoutError ? err.txState : undefined;
|
|
592
|
+
void (async () => {
|
|
593
|
+
// The RPC is likely degraded right after a timeout, so back up without the block number
|
|
594
|
+
// rather than leaking an unhandled rejection.
|
|
595
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber().catch(() => 0n);
|
|
596
|
+
this.backupFailedTx(
|
|
597
|
+
{
|
|
598
|
+
id: this.failureRecordId(
|
|
599
|
+
validRequests.map(r => r.action),
|
|
600
|
+
currentL2Slot,
|
|
601
|
+
),
|
|
602
|
+
failureType: 'timeout',
|
|
603
|
+
request: { to: MULTI_CALL_3_ADDRESS as Hex, data: '0x' as Hex },
|
|
604
|
+
l1BlockNumber,
|
|
605
|
+
error: { message: viemError.message, name: 'TimeoutError' },
|
|
606
|
+
context: {
|
|
607
|
+
actions: validRequests.map(r => r.action),
|
|
608
|
+
requests: validRequests
|
|
609
|
+
.filter(r => r.request.to !== null)
|
|
610
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
611
|
+
sender: this.getSenderAddress().toString(),
|
|
612
|
+
slot: Number(currentL2Slot),
|
|
613
|
+
},
|
|
614
|
+
timing: this.computeTimingInfo(currentL2Slot),
|
|
615
|
+
gasInfo: timeoutState
|
|
616
|
+
? {
|
|
617
|
+
sentGasPriceLadder: timeoutState.gasPriceHistory,
|
|
618
|
+
attempts: timeoutState.attempts,
|
|
619
|
+
gasLimit: timeoutState.gasLimit,
|
|
620
|
+
nonce: timeoutState.nonce,
|
|
621
|
+
}
|
|
622
|
+
: undefined,
|
|
623
|
+
},
|
|
624
|
+
{ captureFeeSummary: true, targetSlot: currentL2Slot },
|
|
625
|
+
);
|
|
626
|
+
})();
|
|
627
|
+
}
|
|
501
628
|
return undefined;
|
|
502
629
|
} finally {
|
|
503
630
|
try {
|
|
@@ -525,23 +652,37 @@ export class SequencerPublisher {
|
|
|
525
652
|
}
|
|
526
653
|
|
|
527
654
|
/** Backs up entries dropped by bundle simulation, one record per dropped action. */
|
|
528
|
-
private async backupDroppedInSim(dropped: DroppedRequest[]): Promise<void> {
|
|
655
|
+
private async backupDroppedInSim(dropped: DroppedRequest[], targetSlot?: SlotNumber): Promise<void> {
|
|
529
656
|
if (dropped.length === 0) {
|
|
530
657
|
return;
|
|
531
658
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
659
|
+
// Invoked as `void backupDroppedInSim(...)` on the publish path, so it must not throw.
|
|
660
|
+
try {
|
|
661
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
662
|
+
// Every dropped entry failed in the same slot against the same L1 fee conditions, so capture
|
|
663
|
+
// the fee environment once and share it rather than re-reading the window per entry.
|
|
664
|
+
const sharedFeeSummary = await this.captureFeeEnvironment(targetSlot).catch(() => undefined);
|
|
665
|
+
const timing = this.computeTimingInfo(targetSlot);
|
|
666
|
+
for (const { request: req } of dropped) {
|
|
667
|
+
this.backupFailedTx(
|
|
668
|
+
{
|
|
669
|
+
id: keccak256(req.request.data!),
|
|
670
|
+
failureType: 'simulation',
|
|
671
|
+
request: { to: req.request.to! as Hex, data: req.request.data! },
|
|
672
|
+
l1BlockNumber,
|
|
673
|
+
error: { message: 'Bundle entry dropped: action reverted in sim' },
|
|
674
|
+
context: {
|
|
675
|
+
actions: [req.action],
|
|
676
|
+
sender: this.getSenderAddress().toString(),
|
|
677
|
+
slot: targetSlot !== undefined ? Number(targetSlot) : undefined,
|
|
678
|
+
},
|
|
679
|
+
timing,
|
|
680
|
+
},
|
|
681
|
+
{ sharedFeeSummary },
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
} catch (err) {
|
|
685
|
+
this.log.warn(`Failed to back up dropped-in-sim entries`, err);
|
|
545
686
|
}
|
|
546
687
|
}
|
|
547
688
|
|
|
@@ -554,6 +695,7 @@ export class SequencerPublisher {
|
|
|
554
695
|
validRequests: RequestWithExpiry[],
|
|
555
696
|
txConfig: RequestWithExpiry['gasConfig'],
|
|
556
697
|
blobConfig: L1BlobInputs | undefined,
|
|
698
|
+
targetSlot?: SlotNumber,
|
|
557
699
|
) {
|
|
558
700
|
if (!txConfig?.gasLimit) {
|
|
559
701
|
throw new Error('gasLimit is required for bundled transactions');
|
|
@@ -590,11 +732,13 @@ export class SequencerPublisher {
|
|
|
590
732
|
this.log.error('Forwarder transaction reverted on-chain; not rotating publisher', err, {
|
|
591
733
|
transactionHash: err.receipt.transactionHash,
|
|
592
734
|
});
|
|
735
|
+
this.backupRevertFailure(validRequests, err, currentPublisher, targetSlot);
|
|
593
736
|
return undefined;
|
|
594
737
|
}
|
|
595
738
|
const viemError = formatViemError(err);
|
|
596
739
|
if (!this.getNextPublisher) {
|
|
597
740
|
this.log.error('Failed to publish bundled transactions', viemError);
|
|
741
|
+
this.backupSendFailure(validRequests, viemError, currentPublisher, targetSlot);
|
|
598
742
|
return undefined;
|
|
599
743
|
}
|
|
600
744
|
this.log.warn(
|
|
@@ -608,6 +752,7 @@ export class SequencerPublisher {
|
|
|
608
752
|
viemError,
|
|
609
753
|
{ triedAddresses: triedAddresses.map(a => a.toString()) },
|
|
610
754
|
);
|
|
755
|
+
this.backupSendFailure(validRequests, viemError, currentPublisher, targetSlot);
|
|
611
756
|
return undefined;
|
|
612
757
|
}
|
|
613
758
|
currentPublisher = nextPublisher;
|
|
@@ -615,35 +760,154 @@ export class SequencerPublisher {
|
|
|
615
760
|
}
|
|
616
761
|
}
|
|
617
762
|
|
|
763
|
+
/** Backs up an on-chain revert failure to the failed tx store. */
|
|
764
|
+
private backupRevertFailure(
|
|
765
|
+
requests: RequestWithExpiry[],
|
|
766
|
+
err: MulticallForwarderRevertedError,
|
|
767
|
+
publisher: L1TxUtils,
|
|
768
|
+
targetSlot?: SlotNumber,
|
|
769
|
+
): void {
|
|
770
|
+
this.backupFailedTx(
|
|
771
|
+
{
|
|
772
|
+
id: err.receipt.transactionHash,
|
|
773
|
+
failureType: 'revert',
|
|
774
|
+
request: { to: MULTI_CALL_3_ADDRESS as Hex, data: '0x' as Hex },
|
|
775
|
+
l1BlockNumber: err.receipt.blockNumber,
|
|
776
|
+
receipt: {
|
|
777
|
+
transactionHash: err.receipt.transactionHash,
|
|
778
|
+
blockNumber: err.receipt.blockNumber,
|
|
779
|
+
gasUsed: err.receipt.gasUsed,
|
|
780
|
+
status: 'reverted',
|
|
781
|
+
},
|
|
782
|
+
error: { message: err.message, name: err.name },
|
|
783
|
+
context: {
|
|
784
|
+
actions: requests.map(r => r.action),
|
|
785
|
+
requests: requests
|
|
786
|
+
.filter(r => r.request.to !== null)
|
|
787
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
788
|
+
sender: publisher.getSenderAddress().toString(),
|
|
789
|
+
slot: targetSlot !== undefined ? Number(targetSlot) : undefined,
|
|
790
|
+
},
|
|
791
|
+
gasInfo: err.txState
|
|
792
|
+
? {
|
|
793
|
+
sentGasPrice: err.txState.gasPrice,
|
|
794
|
+
gasLimit: err.txState.gasLimit,
|
|
795
|
+
nonce: err.txState.nonce,
|
|
796
|
+
}
|
|
797
|
+
: undefined,
|
|
798
|
+
timing: this.computeTimingInfo(targetSlot),
|
|
799
|
+
},
|
|
800
|
+
{ captureFeeSummary: true, targetSlot },
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Backs up a send failure (tx never reached chain) to the failed tx store. */
|
|
805
|
+
private backupSendFailure(
|
|
806
|
+
requests: RequestWithExpiry[],
|
|
807
|
+
error: FormattedViemError | Error,
|
|
808
|
+
publisher: L1TxUtils,
|
|
809
|
+
targetSlot?: SlotNumber,
|
|
810
|
+
): void {
|
|
811
|
+
// If we can't get the block number, still back up without it.
|
|
812
|
+
void this.l1TxUtils
|
|
813
|
+
.getBlockNumber()
|
|
814
|
+
.catch(() => 0n)
|
|
815
|
+
.then(l1BlockNumber => {
|
|
816
|
+
this.backupFailedTx(
|
|
817
|
+
{
|
|
818
|
+
id: this.failureRecordId(
|
|
819
|
+
requests.map(r => r.action),
|
|
820
|
+
targetSlot,
|
|
821
|
+
),
|
|
822
|
+
failureType: 'send-error',
|
|
823
|
+
request: { to: MULTI_CALL_3_ADDRESS as Hex, data: '0x' as Hex },
|
|
824
|
+
l1BlockNumber,
|
|
825
|
+
error: {
|
|
826
|
+
message: error.message,
|
|
827
|
+
name: 'name' in error ? error.name : undefined,
|
|
828
|
+
},
|
|
829
|
+
context: {
|
|
830
|
+
actions: requests.map(r => r.action),
|
|
831
|
+
requests: requests
|
|
832
|
+
.filter(r => r.request.to !== null)
|
|
833
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
834
|
+
sender: publisher.getSenderAddress().toString(),
|
|
835
|
+
slot: targetSlot !== undefined ? Number(targetSlot) : undefined,
|
|
836
|
+
},
|
|
837
|
+
timing: this.computeTimingInfo(targetSlot),
|
|
838
|
+
},
|
|
839
|
+
{ captureFeeSummary: true, targetSlot },
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
618
844
|
/*
|
|
619
845
|
* 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
846
|
*/
|
|
626
847
|
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;
|
|
633
|
-
const sleepMs = submitAfterMs - this.dateProvider.now();
|
|
634
|
-
if (sleepMs > 0) {
|
|
635
|
-
this.log.debug(`Sleeping ${sleepMs}ms before sending requests`, {
|
|
636
|
-
targetSlot,
|
|
637
|
-
submitAfterMs,
|
|
638
|
-
});
|
|
639
|
-
await this.interruptibleSleep.sleep(sleepMs);
|
|
640
|
-
}
|
|
848
|
+
await this.waitForTargetSlot(targetSlot);
|
|
641
849
|
if (this.interrupted) {
|
|
642
850
|
return undefined;
|
|
643
851
|
}
|
|
852
|
+
|
|
644
853
|
return this.sendRequests(targetSlot);
|
|
645
854
|
}
|
|
646
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
|
|
858
|
+
* to be mined, so we don't risk being included in it. If that block never gets mined after
|
|
859
|
+
* a timeout, we assume it got skipped on L1, so we send the tx anyway.
|
|
860
|
+
*/
|
|
861
|
+
private async waitForTargetSlot(targetSlot: SlotNumber): Promise<void> {
|
|
862
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
863
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
864
|
+
const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
|
|
865
|
+
const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
|
|
866
|
+
const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
|
|
867
|
+
const logCtx = { targetSlot, startOfTargetSlotTs, nowInSeconds, previousL1BlockTs, waitDeadlineTs };
|
|
868
|
+
|
|
869
|
+
// Check if we are already past time
|
|
870
|
+
if (nowInSeconds >= startOfTargetSlotTs) {
|
|
871
|
+
this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Otherwise we wait
|
|
876
|
+
this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
|
|
877
|
+
|
|
878
|
+
// Wait until previous L1 block timestamp first
|
|
879
|
+
const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
|
|
880
|
+
if (sleepMs > 0 && !this.interrupted) {
|
|
881
|
+
this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
|
|
882
|
+
await this.interruptibleSleep.sleep(sleepMs);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Then loop until we see the previous L1 block, so we know that we cannot be included in it.
|
|
886
|
+
// We time out after a while, once we are sure that that block is skipped in L1.
|
|
887
|
+
while (!this.interrupted) {
|
|
888
|
+
try {
|
|
889
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
890
|
+
logCtx.nowInSeconds = nowInSeconds;
|
|
891
|
+
|
|
892
|
+
if (nowInSeconds >= waitDeadlineTs) {
|
|
893
|
+
this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const latestBlockTs = await this.l1TxUtils.getBlock().then(b => b.timestamp);
|
|
898
|
+
if (latestBlockTs >= previousL1BlockTs) {
|
|
899
|
+
this.log.debug(`Previous L1 block mined, proceeding to send requests`, { ...logCtx, latestBlockTs });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
this.log.trace(`Previous L1 block not mined yet, continuing to wait`, { ...logCtx, latestBlockTs });
|
|
903
|
+
} catch (err) {
|
|
904
|
+
this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
|
|
905
|
+
} finally {
|
|
906
|
+
await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
647
911
|
private callbackBundledTransactions(
|
|
648
912
|
requests: RequestWithExpiry[],
|
|
649
913
|
result: { receipt: TransactionReceipt; multicallData: Hex },
|
|
@@ -684,8 +948,7 @@ export class SequencerPublisher {
|
|
|
684
948
|
// skip the proposal; we do NOT treat these as bugs.
|
|
685
949
|
const expectedErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
686
950
|
|
|
687
|
-
const
|
|
688
|
-
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
951
|
+
const slotOffset = this.aztecSlotDuration;
|
|
689
952
|
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
690
953
|
|
|
691
954
|
return this.rollupContract
|
|
@@ -839,18 +1102,22 @@ export class SequencerPublisher {
|
|
|
839
1102
|
|
|
840
1103
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
841
1104
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
842
|
-
this.backupFailedTx(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1105
|
+
this.backupFailedTx(
|
|
1106
|
+
{
|
|
1107
|
+
id: keccak256(request.data!),
|
|
1108
|
+
failureType: 'simulation',
|
|
1109
|
+
request: { to: request.to!, data: request.data!, value: request.value },
|
|
1110
|
+
l1BlockNumber,
|
|
1111
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1112
|
+
context: {
|
|
1113
|
+
actions: [`invalidate-${reason}`],
|
|
1114
|
+
checkpointNumber,
|
|
1115
|
+
sender: this.getSenderAddress().toString(),
|
|
1116
|
+
},
|
|
1117
|
+
timing: this.computeTimingInfo(this.getCurrentL2Slot()),
|
|
852
1118
|
},
|
|
853
|
-
|
|
1119
|
+
{ captureFeeSummary: true, targetSlot: this.getCurrentL2Slot() },
|
|
1120
|
+
);
|
|
854
1121
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
855
1122
|
}
|
|
856
1123
|
}
|
|
@@ -864,7 +1131,11 @@ export class SequencerPublisher {
|
|
|
864
1131
|
const logData = { ...checkpoint, reason };
|
|
865
1132
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
866
1133
|
|
|
867
|
-
|
|
1134
|
+
// Use the exact packed tuple posted to L1 verbatim. A repack via `packAttestations` is not a
|
|
1135
|
+
// byte-faithful inverse of `fromPacked` (a canonicalized yParity byte or an all-zero signature slot
|
|
1136
|
+
// round-trips differently), so it would diverge from the stored `attestationsHash` and revert the
|
|
1137
|
+
// invalidation.
|
|
1138
|
+
const attestationsAndSigners = validationResult.verbatimAttestations;
|
|
868
1139
|
|
|
869
1140
|
if (reason === 'invalid-attestation') {
|
|
870
1141
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -904,6 +1175,19 @@ export class SequencerPublisher {
|
|
|
904
1175
|
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
905
1176
|
return false;
|
|
906
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
const canonicalRollup = await base.getRollupAddress();
|
|
1180
|
+
if (!canonicalRollup.equals(EthAddress.fromString(this.rollupContract.address))) {
|
|
1181
|
+
this.log.warn(`Rollup ${this.rollupContract.address} is not canonical, skipping governance signal`, {
|
|
1182
|
+
slotNumber,
|
|
1183
|
+
signalType,
|
|
1184
|
+
canonicalRollup,
|
|
1185
|
+
targetRollup: this.rollupContract.address,
|
|
1186
|
+
payload: payload.toString(),
|
|
1187
|
+
});
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
907
1191
|
const round = await base.computeRound(slotNumber);
|
|
908
1192
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
909
1193
|
|
|
@@ -920,23 +1204,38 @@ export class SequencerPublisher {
|
|
|
920
1204
|
return false;
|
|
921
1205
|
}
|
|
922
1206
|
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
|
|
927
|
-
let proposed = false;
|
|
1207
|
+
// Classify the payload against the Governance proposal history so we stop signalling once its
|
|
1208
|
+
// proposal is live or was already executed, while still re-signalling one whose proposal was
|
|
1209
|
+
// merely rejected/dropped/expired.
|
|
1210
|
+
let status: PayloadProposalStatus = 'none';
|
|
928
1211
|
try {
|
|
929
|
-
|
|
1212
|
+
status = await base.getPayloadProposalStatus(payload.toString());
|
|
930
1213
|
} catch (err) {
|
|
931
1214
|
// We deliberately swallow the error and proceed to signal. Failing closed (skipping the
|
|
932
1215
|
// signal) on transient RPC errors would let a flaky L1 endpoint silence governance
|
|
933
1216
|
// participation entirely; failing open at worst produces a duplicate signal that the
|
|
934
1217
|
// contract will simply count alongside others in the round.
|
|
935
|
-
this.log.error(`Failed to check
|
|
1218
|
+
this.log.error(`Failed to check governance proposal status for payload ${payload} (signalling anyway)`, err, {
|
|
1219
|
+
slotNumber,
|
|
1220
|
+
signalType,
|
|
1221
|
+
});
|
|
936
1222
|
}
|
|
937
1223
|
|
|
938
|
-
if (
|
|
939
|
-
this.log.info(`Payload ${payload} has a live governance proposal, stopping signals
|
|
1224
|
+
if (status === 'live') {
|
|
1225
|
+
this.log.info(`Payload ${payload} has a live governance proposal, stopping signals`, {
|
|
1226
|
+
slotNumber,
|
|
1227
|
+
signalType,
|
|
1228
|
+
payload: payload.toString(),
|
|
1229
|
+
});
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (status === 'executed' && !this.config.governanceProposerForcePayloadVote) {
|
|
1234
|
+
this.log.info(
|
|
1235
|
+
`Payload ${payload} was executed by governance within lookback, stopping signals ` +
|
|
1236
|
+
`(set GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE to re-signal)`,
|
|
1237
|
+
{ slotNumber, signalType, payload: payload.toString() },
|
|
1238
|
+
);
|
|
940
1239
|
return false;
|
|
941
1240
|
}
|
|
942
1241
|
|
|
@@ -1013,6 +1312,47 @@ export class SequencerPublisher {
|
|
|
1013
1312
|
);
|
|
1014
1313
|
}
|
|
1015
1314
|
|
|
1315
|
+
/**
|
|
1316
|
+
* Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
|
|
1317
|
+
* `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
|
|
1318
|
+
* bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
|
|
1319
|
+
* as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
|
|
1320
|
+
* (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
|
|
1321
|
+
* chain (e.g. bad data blocking sync) can be wound back to recover.
|
|
1322
|
+
* @returns true if a prune request was enqueued, false otherwise.
|
|
1323
|
+
*/
|
|
1324
|
+
public async enqueuePruneIfPrunable(slotNumber: SlotNumber): Promise<boolean> {
|
|
1325
|
+
if (this.lastActions['prune'] === slotNumber) {
|
|
1326
|
+
this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, { slotNumber });
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
// Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
|
|
1330
|
+
// (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
|
|
1331
|
+
// and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
|
|
1332
|
+
// derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
|
|
1333
|
+
// the timestamp more granularly.
|
|
1334
|
+
const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
|
|
1335
|
+
const canPrune = await this.rollupContract.canPruneAtTime(ts).catch(err => {
|
|
1336
|
+
this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, { slotNumber });
|
|
1337
|
+
return false;
|
|
1338
|
+
});
|
|
1339
|
+
if (!canPrune) {
|
|
1340
|
+
this.log.debug(`Rollup not prunable at slot ${slotNumber}`, { slotNumber });
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
const request: L1TxRequest = {
|
|
1344
|
+
to: this.rollupContract.address,
|
|
1345
|
+
data: encodeFunctionData({ abi: RollupAbi, functionName: 'prune', args: [] }),
|
|
1346
|
+
};
|
|
1347
|
+
this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, { slotNumber });
|
|
1348
|
+
return this.enqueueRequest(
|
|
1349
|
+
'prune',
|
|
1350
|
+
request,
|
|
1351
|
+
{ address: this.rollupContract.address, abi: RollupAbi, eventName: 'PrunedPending' },
|
|
1352
|
+
slotNumber,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1016
1356
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
1017
1357
|
public async enqueueSlashingActions(
|
|
1018
1358
|
actions: ProposerSlashAction[],
|
|
@@ -1251,18 +1591,22 @@ export class SequencerPublisher {
|
|
|
1251
1591
|
args: [blobInput],
|
|
1252
1592
|
});
|
|
1253
1593
|
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1254
|
-
this.backupFailedTx(
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1594
|
+
this.backupFailedTx(
|
|
1595
|
+
{
|
|
1596
|
+
id: keccak256(validateBlobsData),
|
|
1597
|
+
failureType: 'simulation',
|
|
1598
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1599
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1600
|
+
l1BlockNumber,
|
|
1601
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1602
|
+
context: {
|
|
1603
|
+
actions: ['validate-blobs'],
|
|
1604
|
+
sender: this.getSenderAddress().toString(),
|
|
1605
|
+
},
|
|
1606
|
+
timing: this.computeTimingInfo(this.getCurrentL2Slot()),
|
|
1264
1607
|
},
|
|
1265
|
-
|
|
1608
|
+
{ captureFeeSummary: true, targetSlot: this.getCurrentL2Slot() },
|
|
1609
|
+
);
|
|
1266
1610
|
throw new Error('Failed to validate blobs');
|
|
1267
1611
|
});
|
|
1268
1612
|
}
|