@aztec/sequencer-client 0.0.1-commit.3100065 → 0.0.1-commit.330febf
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/config.d.ts +2 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +6 -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 +20 -4
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +278 -49
- package/dest/sequencer/automine/automine_sequencer.d.ts +1 -1
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +7 -1
- package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +9 -1
- package/dest/sequencer/errors.d.ts +8 -1
- package/dest/sequencer/errors.d.ts.map +1 -1
- package/dest/sequencer/errors.js +9 -0
- 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/sequencer.d.ts +4 -3
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +13 -4
- package/package.json +28 -28
- package/src/config.ts +7 -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 +308 -59
- package/src/sequencer/README.md +243 -0
- package/src/sequencer/automine/automine_sequencer.ts +5 -1
- package/src/sequencer/checkpoint_proposal_job.ts +5 -1
- package/src/sequencer/errors.ts +15 -0
- package/src/sequencer/missing_committee.ts +192 -0
- package/src/sequencer/sequencer.ts +14 -5
|
@@ -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,
|
|
@@ -215,6 +218,7 @@ export class SequencerPublisher {
|
|
|
215
218
|
| 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
|
|
216
219
|
| 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
|
|
217
220
|
> &
|
|
221
|
+
Pick<SequencerConfig, 'governanceProposerForcePayloadVote'> &
|
|
218
222
|
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
219
223
|
deps: {
|
|
220
224
|
telemetry?: TelemetryClient;
|
|
@@ -287,25 +291,97 @@ export class SequencerPublisher {
|
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
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.
|
|
292
298
|
*/
|
|
293
|
-
private backupFailedTx(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
299
|
+
private backupFailedTx(
|
|
300
|
+
failedTx: Omit<FailedL1Tx, 'timestamp'>,
|
|
301
|
+
opts?: { captureFeeSummary?: boolean; targetSlot?: SlotNumber; sharedFeeSummary?: FailedL1Tx['gasInfo'] },
|
|
302
|
+
): void {
|
|
298
303
|
const tx: FailedL1Tx = {
|
|
299
304
|
...failedTx,
|
|
300
305
|
timestamp: Date.now(),
|
|
301
306
|
};
|
|
302
307
|
|
|
303
308
|
// Fire and forget - don't block on backup
|
|
304
|
-
void
|
|
305
|
-
|
|
306
|
-
|
|
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) {
|
|
307
335
|
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
308
|
-
}
|
|
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()}`));
|
|
309
385
|
}
|
|
310
386
|
|
|
311
387
|
public getRollupContract(): RollupContract {
|
|
@@ -469,7 +545,7 @@ export class SequencerPublisher {
|
|
|
469
545
|
|
|
470
546
|
if (bundleResult.kind === 'aborted') {
|
|
471
547
|
this.logDroppedInSim(bundleResult.droppedRequests);
|
|
472
|
-
void this.backupDroppedInSim(bundleResult.droppedRequests).catch(err =>
|
|
548
|
+
void this.backupDroppedInSim(bundleResult.droppedRequests, currentL2Slot).catch(err =>
|
|
473
549
|
this.log.error(`Failed to backup requests dropped in simulation`, err),
|
|
474
550
|
);
|
|
475
551
|
return undefined;
|
|
@@ -495,7 +571,7 @@ export class SequencerPublisher {
|
|
|
495
571
|
requests: requests.map(request => request.action),
|
|
496
572
|
txConfig,
|
|
497
573
|
});
|
|
498
|
-
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
|
|
574
|
+
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig, currentL2Slot);
|
|
499
575
|
if (result === undefined) {
|
|
500
576
|
return undefined;
|
|
501
577
|
}
|
|
@@ -511,6 +587,44 @@ export class SequencerPublisher {
|
|
|
511
587
|
} catch (err) {
|
|
512
588
|
const viemError = formatViemError(err);
|
|
513
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
|
+
}
|
|
514
628
|
return undefined;
|
|
515
629
|
} finally {
|
|
516
630
|
try {
|
|
@@ -538,23 +652,37 @@ export class SequencerPublisher {
|
|
|
538
652
|
}
|
|
539
653
|
|
|
540
654
|
/** Backs up entries dropped by bundle simulation, one record per dropped action. */
|
|
541
|
-
private async backupDroppedInSim(dropped: DroppedRequest[]): Promise<void> {
|
|
655
|
+
private async backupDroppedInSim(dropped: DroppedRequest[], targetSlot?: SlotNumber): Promise<void> {
|
|
542
656
|
if (dropped.length === 0) {
|
|
543
657
|
return;
|
|
544
658
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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);
|
|
558
686
|
}
|
|
559
687
|
}
|
|
560
688
|
|
|
@@ -567,6 +695,7 @@ export class SequencerPublisher {
|
|
|
567
695
|
validRequests: RequestWithExpiry[],
|
|
568
696
|
txConfig: RequestWithExpiry['gasConfig'],
|
|
569
697
|
blobConfig: L1BlobInputs | undefined,
|
|
698
|
+
targetSlot?: SlotNumber,
|
|
570
699
|
) {
|
|
571
700
|
if (!txConfig?.gasLimit) {
|
|
572
701
|
throw new Error('gasLimit is required for bundled transactions');
|
|
@@ -603,11 +732,13 @@ export class SequencerPublisher {
|
|
|
603
732
|
this.log.error('Forwarder transaction reverted on-chain; not rotating publisher', err, {
|
|
604
733
|
transactionHash: err.receipt.transactionHash,
|
|
605
734
|
});
|
|
735
|
+
this.backupRevertFailure(validRequests, err, currentPublisher, targetSlot);
|
|
606
736
|
return undefined;
|
|
607
737
|
}
|
|
608
738
|
const viemError = formatViemError(err);
|
|
609
739
|
if (!this.getNextPublisher) {
|
|
610
740
|
this.log.error('Failed to publish bundled transactions', viemError);
|
|
741
|
+
this.backupSendFailure(validRequests, viemError, currentPublisher, targetSlot);
|
|
611
742
|
return undefined;
|
|
612
743
|
}
|
|
613
744
|
this.log.warn(
|
|
@@ -621,6 +752,7 @@ export class SequencerPublisher {
|
|
|
621
752
|
viemError,
|
|
622
753
|
{ triedAddresses: triedAddresses.map(a => a.toString()) },
|
|
623
754
|
);
|
|
755
|
+
this.backupSendFailure(validRequests, viemError, currentPublisher, targetSlot);
|
|
624
756
|
return undefined;
|
|
625
757
|
}
|
|
626
758
|
currentPublisher = nextPublisher;
|
|
@@ -628,6 +760,87 @@ export class SequencerPublisher {
|
|
|
628
760
|
}
|
|
629
761
|
}
|
|
630
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
|
+
|
|
631
844
|
/*
|
|
632
845
|
* Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
|
|
633
846
|
*/
|
|
@@ -889,18 +1102,22 @@ export class SequencerPublisher {
|
|
|
889
1102
|
|
|
890
1103
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
891
1104
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
892
|
-
this.backupFailedTx(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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()),
|
|
902
1118
|
},
|
|
903
|
-
|
|
1119
|
+
{ captureFeeSummary: true, targetSlot: this.getCurrentL2Slot() },
|
|
1120
|
+
);
|
|
904
1121
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
905
1122
|
}
|
|
906
1123
|
}
|
|
@@ -958,6 +1175,19 @@ export class SequencerPublisher {
|
|
|
958
1175
|
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
959
1176
|
return false;
|
|
960
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
|
+
|
|
961
1191
|
const round = await base.computeRound(slotNumber);
|
|
962
1192
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
963
1193
|
|
|
@@ -974,23 +1204,38 @@ export class SequencerPublisher {
|
|
|
974
1204
|
return false;
|
|
975
1205
|
}
|
|
976
1206
|
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
|
|
981
|
-
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';
|
|
982
1211
|
try {
|
|
983
|
-
|
|
1212
|
+
status = await base.getPayloadProposalStatus(payload.toString());
|
|
984
1213
|
} catch (err) {
|
|
985
1214
|
// We deliberately swallow the error and proceed to signal. Failing closed (skipping the
|
|
986
1215
|
// signal) on transient RPC errors would let a flaky L1 endpoint silence governance
|
|
987
1216
|
// participation entirely; failing open at worst produces a duplicate signal that the
|
|
988
1217
|
// contract will simply count alongside others in the round.
|
|
989
|
-
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
|
+
});
|
|
990
1222
|
}
|
|
991
1223
|
|
|
992
|
-
if (
|
|
993
|
-
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
|
+
);
|
|
994
1239
|
return false;
|
|
995
1240
|
}
|
|
996
1241
|
|
|
@@ -1346,18 +1591,22 @@ export class SequencerPublisher {
|
|
|
1346
1591
|
args: [blobInput],
|
|
1347
1592
|
});
|
|
1348
1593
|
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1349
|
-
this.backupFailedTx(
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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()),
|
|
1359
1607
|
},
|
|
1360
|
-
|
|
1608
|
+
{ captureFeeSummary: true, targetSlot: this.getCurrentL2Slot() },
|
|
1609
|
+
);
|
|
1361
1610
|
throw new Error('Failed to validate blobs');
|
|
1362
1611
|
});
|
|
1363
1612
|
}
|