@aztec/sequencer-client 0.0.1-commit.1142ef1 → 0.0.1-commit.125b3452
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +24 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +101 -16
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +49 -30
- package/dest/global_variable_builder/global_builder.d.ts +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +7 -6
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +26 -8
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +349 -49
- package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +217 -66
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -1
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +111 -30
- package/dest/sequencer/sequencer.d.ts +42 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +107 -50
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +27 -19
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +67 -38
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +12 -11
- package/package.json +29 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +65 -41
- package/src/global_variable_builder/global_builder.ts +6 -5
- package/src/index.ts +1 -6
- package/src/publisher/config.ts +121 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +344 -61
- package/src/sequencer/checkpoint_proposal_job.ts +306 -81
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +124 -32
- package/src/sequencer/sequencer.ts +131 -53
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +122 -78
- package/src/test/utils.ts +24 -14
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -372,24 +372,28 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
|
|
|
372
372
|
}
|
|
373
373
|
var _dec, _dec1, _dec2, _initProto;
|
|
374
374
|
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
375
|
-
import { MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
|
|
375
|
+
import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
|
|
376
376
|
import { L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
377
|
-
import { WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
|
|
378
|
-
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
377
|
+
import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
|
|
378
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
379
379
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
380
380
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
381
381
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
382
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
382
383
|
import { pick } from '@aztec/foundation/collection';
|
|
384
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
383
385
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
384
386
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
385
387
|
import { createLogger } from '@aztec/foundation/log';
|
|
388
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
386
389
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
387
390
|
import { Timer } from '@aztec/foundation/timer';
|
|
388
391
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
389
392
|
import { encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
390
393
|
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
391
394
|
import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
392
|
-
import { encodeFunctionData, toHex } from 'viem';
|
|
395
|
+
import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
|
|
396
|
+
import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
393
397
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
394
398
|
export const Actions = [
|
|
395
399
|
'invalidate-by-invalid-attestation',
|
|
@@ -429,19 +433,19 @@ export class SequencerPublisher {
|
|
|
429
433
|
interrupted;
|
|
430
434
|
metrics;
|
|
431
435
|
epochCache;
|
|
436
|
+
failedTxStore;
|
|
432
437
|
governanceLog;
|
|
433
438
|
slashingLog;
|
|
434
439
|
lastActions;
|
|
435
440
|
isPayloadEmptyCache;
|
|
441
|
+
payloadProposedCache;
|
|
436
442
|
log;
|
|
437
443
|
ethereumSlotDuration;
|
|
438
444
|
blobClient;
|
|
439
445
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
446
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
|
|
440
447
|
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
441
|
-
|
|
442
|
-
// Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
|
|
443
|
-
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
444
|
-
static PROPOSE_GAS_GUESS = 12_000_000n;
|
|
448
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
|
|
445
449
|
// A CALL to a cold address is 2700 gas
|
|
446
450
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
447
451
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -460,6 +464,7 @@ export class SequencerPublisher {
|
|
|
460
464
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
461
465
|
this.lastActions = {};
|
|
462
466
|
this.isPayloadEmptyCache = new Map();
|
|
467
|
+
this.payloadProposedCache = new Set();
|
|
463
468
|
this.requests = [];
|
|
464
469
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
465
470
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
@@ -470,6 +475,7 @@ export class SequencerPublisher {
|
|
|
470
475
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
471
476
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
472
477
|
this.l1TxUtils = deps.l1TxUtils;
|
|
478
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
473
479
|
this.rollupContract = deps.rollupContract;
|
|
474
480
|
this.govProposerContract = deps.governanceProposerContract;
|
|
475
481
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -483,10 +489,36 @@ export class SequencerPublisher {
|
|
|
483
489
|
if (config.fishermanMode) {
|
|
484
490
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
485
491
|
}
|
|
492
|
+
// Initialize fee asset price oracle
|
|
493
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
|
|
494
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
495
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
499
|
+
* Does nothing if no store is configured.
|
|
500
|
+
*/ backupFailedTx(failedTx) {
|
|
501
|
+
if (!this.failedTxStore) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const tx = {
|
|
505
|
+
...failedTx,
|
|
506
|
+
timestamp: Date.now()
|
|
507
|
+
};
|
|
508
|
+
// Fire and forget - don't block on backup
|
|
509
|
+
void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
|
|
510
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
511
|
+
});
|
|
486
512
|
}
|
|
487
513
|
getRollupContract() {
|
|
488
514
|
return this.rollupContract;
|
|
489
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Gets the fee asset price modifier from the oracle.
|
|
518
|
+
* Returns 0n if the oracle query fails.
|
|
519
|
+
*/ getFeeAssetPriceModifier() {
|
|
520
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
521
|
+
}
|
|
490
522
|
getSenderAddress() {
|
|
491
523
|
return this.l1TxUtils.getSenderAddress();
|
|
492
524
|
}
|
|
@@ -544,7 +576,7 @@ export class SequencerPublisher {
|
|
|
544
576
|
// Get the transaction requests
|
|
545
577
|
const l1Requests = requestsToAnalyze.map((r)=>r.request);
|
|
546
578
|
// Start the analysis
|
|
547
|
-
const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit :
|
|
579
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
|
|
548
580
|
this.log.info('Started L1 fee analysis', {
|
|
549
581
|
analysisId,
|
|
550
582
|
l2SlotNumber: l2SlotNumber.toString(),
|
|
@@ -602,7 +634,16 @@ export class SequencerPublisher {
|
|
|
602
634
|
const blobConfig = blobConfigs[0];
|
|
603
635
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
604
636
|
const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
|
|
605
|
-
|
|
637
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
638
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
639
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
640
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
641
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
642
|
+
requested: gasLimit,
|
|
643
|
+
capped: maxGas
|
|
644
|
+
});
|
|
645
|
+
gasLimit = maxGas;
|
|
646
|
+
}
|
|
606
647
|
const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
|
|
607
648
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
|
|
608
649
|
const txConfig = {
|
|
@@ -613,12 +654,34 @@ export class SequencerPublisher {
|
|
|
613
654
|
// This ensures the committee gets precomputed correctly
|
|
614
655
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
615
656
|
try {
|
|
657
|
+
// Capture context for failed tx backup before sending
|
|
658
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
659
|
+
const multicallData = encodeFunctionData({
|
|
660
|
+
abi: multicall3Abi,
|
|
661
|
+
functionName: 'aggregate3',
|
|
662
|
+
args: [
|
|
663
|
+
validRequests.map((r)=>({
|
|
664
|
+
target: r.request.to,
|
|
665
|
+
callData: r.request.data,
|
|
666
|
+
allowFailure: true
|
|
667
|
+
}))
|
|
668
|
+
]
|
|
669
|
+
});
|
|
670
|
+
const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
|
|
671
|
+
const txContext = {
|
|
672
|
+
multicallData,
|
|
673
|
+
blobData: blobDataHex,
|
|
674
|
+
l1BlockNumber
|
|
675
|
+
};
|
|
616
676
|
this.log.debug('Forwarding transactions', {
|
|
617
677
|
validRequests: validRequests.map((request)=>request.action),
|
|
618
678
|
txConfig
|
|
619
679
|
});
|
|
620
|
-
const result = await
|
|
621
|
-
|
|
680
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
681
|
+
if (result === undefined) {
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
|
|
622
685
|
return {
|
|
623
686
|
result,
|
|
624
687
|
expiredActions,
|
|
@@ -638,17 +701,83 @@ export class SequencerPublisher {
|
|
|
638
701
|
}
|
|
639
702
|
}
|
|
640
703
|
}
|
|
641
|
-
|
|
704
|
+
/**
|
|
705
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
706
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
707
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
708
|
+
*/ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
|
|
709
|
+
const triedAddresses = [];
|
|
710
|
+
let currentPublisher = this.l1TxUtils;
|
|
711
|
+
while(true){
|
|
712
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
713
|
+
try {
|
|
714
|
+
const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
|
|
715
|
+
this.l1TxUtils = currentPublisher;
|
|
716
|
+
return result;
|
|
717
|
+
} catch (err) {
|
|
718
|
+
if (err instanceof TimeoutError) {
|
|
719
|
+
throw err;
|
|
720
|
+
}
|
|
721
|
+
const viemError = formatViemError(err);
|
|
722
|
+
if (!this.getNextPublisher) {
|
|
723
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
|
|
727
|
+
const nextPublisher = await this.getNextPublisher([
|
|
728
|
+
...triedAddresses
|
|
729
|
+
]);
|
|
730
|
+
if (!nextPublisher) {
|
|
731
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
currentPublisher = nextPublisher;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
callbackBundledTransactions(requests, result, txContext) {
|
|
642
739
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
643
740
|
if (result instanceof FormattedViemError) {
|
|
644
741
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
742
|
+
this.backupFailedTx({
|
|
743
|
+
id: keccak256(txContext.multicallData),
|
|
744
|
+
failureType: 'send-error',
|
|
745
|
+
request: {
|
|
746
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
747
|
+
data: txContext.multicallData
|
|
748
|
+
},
|
|
749
|
+
blobData: txContext.blobData,
|
|
750
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
751
|
+
error: {
|
|
752
|
+
message: result.message,
|
|
753
|
+
name: result.name
|
|
754
|
+
},
|
|
755
|
+
context: {
|
|
756
|
+
actions: requests.map((r)=>r.action),
|
|
757
|
+
requests: requests.map((r)=>({
|
|
758
|
+
action: r.action,
|
|
759
|
+
to: r.request.to,
|
|
760
|
+
data: r.request.data
|
|
761
|
+
})),
|
|
762
|
+
sender: this.getSenderAddress().toString()
|
|
763
|
+
}
|
|
764
|
+
});
|
|
645
765
|
return {
|
|
646
766
|
failedActions: requests.map((r)=>r.action)
|
|
647
767
|
};
|
|
648
768
|
} else {
|
|
649
769
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
650
770
|
result,
|
|
651
|
-
requests
|
|
771
|
+
requests: requests.map((r)=>({
|
|
772
|
+
...r,
|
|
773
|
+
// Avoid logging large blob data
|
|
774
|
+
blobConfig: r.blobConfig ? {
|
|
775
|
+
...r.blobConfig,
|
|
776
|
+
blobs: r.blobConfig.blobs.map((b)=>({
|
|
777
|
+
size: trimmedBytesLength(b)
|
|
778
|
+
}))
|
|
779
|
+
} : undefined
|
|
780
|
+
}))
|
|
652
781
|
});
|
|
653
782
|
const successfulActions = [];
|
|
654
783
|
const failedActions = [];
|
|
@@ -659,6 +788,37 @@ export class SequencerPublisher {
|
|
|
659
788
|
failedActions.push(request.action);
|
|
660
789
|
}
|
|
661
790
|
}
|
|
791
|
+
// Single backup for the whole reverted tx
|
|
792
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
793
|
+
this.backupFailedTx({
|
|
794
|
+
id: result.receipt.transactionHash,
|
|
795
|
+
failureType: 'revert',
|
|
796
|
+
request: {
|
|
797
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
798
|
+
data: txContext.multicallData
|
|
799
|
+
},
|
|
800
|
+
blobData: txContext.blobData,
|
|
801
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
802
|
+
receipt: {
|
|
803
|
+
transactionHash: result.receipt.transactionHash,
|
|
804
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
805
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
806
|
+
status: 'reverted'
|
|
807
|
+
},
|
|
808
|
+
error: {
|
|
809
|
+
message: result.errorMsg ?? 'Transaction reverted'
|
|
810
|
+
},
|
|
811
|
+
context: {
|
|
812
|
+
actions: failedActions,
|
|
813
|
+
requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
|
|
814
|
+
action: r.action,
|
|
815
|
+
to: r.request.to,
|
|
816
|
+
data: r.request.data
|
|
817
|
+
})),
|
|
818
|
+
sender: this.getSenderAddress().toString()
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
662
822
|
return {
|
|
663
823
|
successfulActions,
|
|
664
824
|
failedActions
|
|
@@ -761,8 +921,12 @@ export class SequencerPublisher {
|
|
|
761
921
|
...logData,
|
|
762
922
|
request
|
|
763
923
|
});
|
|
924
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
764
925
|
try {
|
|
765
|
-
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined,
|
|
926
|
+
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
927
|
+
request.abi ?? [],
|
|
928
|
+
ErrorsAbi
|
|
929
|
+
]));
|
|
766
930
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
767
931
|
...logData,
|
|
768
932
|
request,
|
|
@@ -779,7 +943,7 @@ export class SequencerPublisher {
|
|
|
779
943
|
const viemError = formatViemError(err);
|
|
780
944
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
781
945
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
782
|
-
if (viemError.message?.includes('
|
|
946
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
783
947
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
|
|
784
948
|
...logData,
|
|
785
949
|
request,
|
|
@@ -800,6 +964,27 @@ export class SequencerPublisher {
|
|
|
800
964
|
}
|
|
801
965
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
802
966
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
967
|
+
this.backupFailedTx({
|
|
968
|
+
id: keccak256(request.data),
|
|
969
|
+
failureType: 'simulation',
|
|
970
|
+
request: {
|
|
971
|
+
to: request.to,
|
|
972
|
+
data: request.data,
|
|
973
|
+
value: request.value?.toString()
|
|
974
|
+
},
|
|
975
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
976
|
+
error: {
|
|
977
|
+
message: viemError.message,
|
|
978
|
+
name: viemError.name
|
|
979
|
+
},
|
|
980
|
+
context: {
|
|
981
|
+
actions: [
|
|
982
|
+
`invalidate-${reason}`
|
|
983
|
+
],
|
|
984
|
+
checkpointNumber,
|
|
985
|
+
sender: this.getSenderAddress().toString()
|
|
986
|
+
}
|
|
987
|
+
});
|
|
803
988
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
804
989
|
cause: viemError
|
|
805
990
|
});
|
|
@@ -827,29 +1012,15 @@ export class SequencerPublisher {
|
|
|
827
1012
|
}
|
|
828
1013
|
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
|
|
829
1014
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
830
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
831
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
832
|
-
// so that the committee is recalculated correctly
|
|
833
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
834
|
-
// if (ignoreSignatures) {
|
|
835
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
836
|
-
// if (!committee) {
|
|
837
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
838
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
839
|
-
// }
|
|
840
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
841
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
842
|
-
// );
|
|
843
|
-
// }
|
|
844
1015
|
const blobFields = checkpoint.toBlobFields();
|
|
845
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1016
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
846
1017
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
847
1018
|
const args = [
|
|
848
1019
|
{
|
|
849
1020
|
header: checkpoint.header.toViem(),
|
|
850
1021
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
851
1022
|
oracleInput: {
|
|
852
|
-
feeAssetPriceModifier:
|
|
1023
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
853
1024
|
}
|
|
854
1025
|
},
|
|
855
1026
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -884,6 +1055,28 @@ export class SequencerPublisher {
|
|
|
884
1055
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
885
1056
|
return false;
|
|
886
1057
|
}
|
|
1058
|
+
// Check if payload was already submitted to governance
|
|
1059
|
+
const cacheKey = payload.toString();
|
|
1060
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
1061
|
+
try {
|
|
1062
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
1063
|
+
const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
|
|
1064
|
+
0,
|
|
1065
|
+
1,
|
|
1066
|
+
2
|
|
1067
|
+
]), this.log, true);
|
|
1068
|
+
if (proposed) {
|
|
1069
|
+
this.payloadProposedCache.add(cacheKey);
|
|
1070
|
+
}
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
1077
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
887
1080
|
const cachedLastVote = this.lastActions[signalType];
|
|
888
1081
|
this.lastActions[signalType] = slotNumber;
|
|
889
1082
|
const action = signalType;
|
|
@@ -894,15 +1087,41 @@ export class SequencerPublisher {
|
|
|
894
1087
|
signer: this.l1TxUtils.client.account?.address,
|
|
895
1088
|
lastValidL2Slot: slotNumber
|
|
896
1089
|
});
|
|
1090
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
897
1091
|
try {
|
|
898
1092
|
await this.l1TxUtils.simulate(request, {
|
|
899
1093
|
time: timestamp
|
|
900
|
-
}, [],
|
|
1094
|
+
}, [], mergeAbis([
|
|
1095
|
+
request.abi ?? [],
|
|
1096
|
+
ErrorsAbi
|
|
1097
|
+
]));
|
|
901
1098
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
|
|
902
1099
|
request
|
|
903
1100
|
});
|
|
904
1101
|
} catch (err) {
|
|
905
|
-
|
|
1102
|
+
const viemError = formatViemError(err);
|
|
1103
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
1104
|
+
this.backupFailedTx({
|
|
1105
|
+
id: keccak256(request.data),
|
|
1106
|
+
failureType: 'simulation',
|
|
1107
|
+
request: {
|
|
1108
|
+
to: request.to,
|
|
1109
|
+
data: request.data,
|
|
1110
|
+
value: request.value?.toString()
|
|
1111
|
+
},
|
|
1112
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1113
|
+
error: {
|
|
1114
|
+
message: viemError.message,
|
|
1115
|
+
name: viemError.name
|
|
1116
|
+
},
|
|
1117
|
+
context: {
|
|
1118
|
+
actions: [
|
|
1119
|
+
action
|
|
1120
|
+
],
|
|
1121
|
+
slot: slotNumber,
|
|
1122
|
+
sender: this.getSenderAddress().toString()
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
906
1125
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
907
1126
|
}
|
|
908
1127
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -1041,13 +1260,14 @@ export class SequencerPublisher {
|
|
|
1041
1260
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1042
1261
|
const checkpointHeader = checkpoint.header;
|
|
1043
1262
|
const blobFields = checkpoint.toBlobFields();
|
|
1044
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1263
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1045
1264
|
const proposeTxArgs = {
|
|
1046
1265
|
header: checkpointHeader,
|
|
1047
1266
|
archive: checkpoint.archive.root.toBuffer(),
|
|
1048
1267
|
blobs,
|
|
1049
1268
|
attestationsAndSigners,
|
|
1050
|
-
attestationsAndSignersSignature
|
|
1269
|
+
attestationsAndSignersSignature,
|
|
1270
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1051
1271
|
};
|
|
1052
1272
|
let ts;
|
|
1053
1273
|
try {
|
|
@@ -1123,28 +1343,60 @@ export class SequencerPublisher {
|
|
|
1123
1343
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1124
1344
|
this.lastActions[action] = slotNumber;
|
|
1125
1345
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1346
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1126
1347
|
let gasUsed;
|
|
1348
|
+
const simulateAbi = mergeAbis([
|
|
1349
|
+
request.abi ?? [],
|
|
1350
|
+
ErrorsAbi
|
|
1351
|
+
]);
|
|
1127
1352
|
try {
|
|
1128
1353
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1129
1354
|
time: timestamp
|
|
1130
|
-
}, [],
|
|
1355
|
+
}, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
1131
1356
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1132
1357
|
...logData,
|
|
1133
1358
|
request,
|
|
1134
1359
|
gasUsed
|
|
1135
1360
|
});
|
|
1136
1361
|
} catch (err) {
|
|
1137
|
-
const viemError = formatViemError(err);
|
|
1362
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1138
1363
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1364
|
+
this.backupFailedTx({
|
|
1365
|
+
id: keccak256(request.data),
|
|
1366
|
+
failureType: 'simulation',
|
|
1367
|
+
request: {
|
|
1368
|
+
to: request.to,
|
|
1369
|
+
data: request.data,
|
|
1370
|
+
value: request.value?.toString()
|
|
1371
|
+
},
|
|
1372
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1373
|
+
error: {
|
|
1374
|
+
message: viemError.message,
|
|
1375
|
+
name: viemError.name
|
|
1376
|
+
},
|
|
1377
|
+
context: {
|
|
1378
|
+
actions: [
|
|
1379
|
+
action
|
|
1380
|
+
],
|
|
1381
|
+
slot: slotNumber,
|
|
1382
|
+
sender: this.getSenderAddress().toString()
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1139
1385
|
return false;
|
|
1140
1386
|
}
|
|
1141
1387
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1142
1388
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
|
|
1143
1389
|
logData.gasLimit = gasLimit;
|
|
1390
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1391
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1392
|
+
const requestWithAbi = {
|
|
1393
|
+
...request,
|
|
1394
|
+
abi: simulateAbi
|
|
1395
|
+
};
|
|
1144
1396
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1145
1397
|
this.addRequest({
|
|
1146
1398
|
action,
|
|
1147
|
-
request,
|
|
1399
|
+
request: requestWithAbi,
|
|
1148
1400
|
gasConfig: {
|
|
1149
1401
|
gasLimit
|
|
1150
1402
|
},
|
|
@@ -1208,10 +1460,38 @@ export class SequencerPublisher {
|
|
|
1208
1460
|
}, {}, {
|
|
1209
1461
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1210
1462
|
kzg
|
|
1211
|
-
}).catch((err)=>{
|
|
1212
|
-
const
|
|
1213
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1214
|
-
metaMessages
|
|
1463
|
+
}).catch(async (err)=>{
|
|
1464
|
+
const viemError = formatViemError(err);
|
|
1465
|
+
this.log.error(`Failed to validate blobs`, viemError.message, {
|
|
1466
|
+
metaMessages: viemError.metaMessages
|
|
1467
|
+
});
|
|
1468
|
+
const validateBlobsData = encodeFunctionData({
|
|
1469
|
+
abi: RollupAbi,
|
|
1470
|
+
functionName: 'validateBlobs',
|
|
1471
|
+
args: [
|
|
1472
|
+
blobInput
|
|
1473
|
+
]
|
|
1474
|
+
});
|
|
1475
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1476
|
+
this.backupFailedTx({
|
|
1477
|
+
id: keccak256(validateBlobsData),
|
|
1478
|
+
failureType: 'simulation',
|
|
1479
|
+
request: {
|
|
1480
|
+
to: this.rollupContract.address,
|
|
1481
|
+
data: validateBlobsData
|
|
1482
|
+
},
|
|
1483
|
+
blobData: encodedData.blobs.map((b)=>toHex(b.data)),
|
|
1484
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1485
|
+
error: {
|
|
1486
|
+
message: viemError.message,
|
|
1487
|
+
name: viemError.name
|
|
1488
|
+
},
|
|
1489
|
+
context: {
|
|
1490
|
+
actions: [
|
|
1491
|
+
'validate-blobs'
|
|
1492
|
+
],
|
|
1493
|
+
sender: this.getSenderAddress().toString()
|
|
1494
|
+
}
|
|
1215
1495
|
});
|
|
1216
1496
|
throw new Error('Failed to validate blobs');
|
|
1217
1497
|
});
|
|
@@ -1222,8 +1502,7 @@ export class SequencerPublisher {
|
|
|
1222
1502
|
header: encodedData.header.toViem(),
|
|
1223
1503
|
archive: toHex(encodedData.archive),
|
|
1224
1504
|
oracleInput: {
|
|
1225
|
-
|
|
1226
|
-
feeAssetPriceModifier: 0n
|
|
1505
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier
|
|
1227
1506
|
}
|
|
1228
1507
|
},
|
|
1229
1508
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1272,10 +1551,11 @@ export class SequencerPublisher {
|
|
|
1272
1551
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1273
1552
|
});
|
|
1274
1553
|
}
|
|
1554
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1275
1555
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1276
1556
|
to: this.rollupContract.address,
|
|
1277
1557
|
data: rollupData,
|
|
1278
|
-
gas:
|
|
1558
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1279
1559
|
...this.proposerAddressForSimulation && {
|
|
1280
1560
|
from: this.proposerAddressForSimulation.toString()
|
|
1281
1561
|
}
|
|
@@ -1283,10 +1563,10 @@ export class SequencerPublisher {
|
|
|
1283
1563
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1284
1564
|
time: timestamp + 1n,
|
|
1285
1565
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1286
|
-
gasLimit:
|
|
1566
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1287
1567
|
}, stateOverrides, RollupAbi, {
|
|
1288
1568
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1289
|
-
fallbackGasEstimate:
|
|
1569
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT
|
|
1290
1570
|
}).catch((err)=>{
|
|
1291
1571
|
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1292
1572
|
const viemError = formatViemError(err);
|
|
@@ -1294,11 +1574,31 @@ export class SequencerPublisher {
|
|
|
1294
1574
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1295
1575
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1296
1576
|
return {
|
|
1297
|
-
gasUsed:
|
|
1577
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1298
1578
|
logs: []
|
|
1299
1579
|
};
|
|
1300
1580
|
}
|
|
1301
1581
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1582
|
+
this.backupFailedTx({
|
|
1583
|
+
id: keccak256(rollupData),
|
|
1584
|
+
failureType: 'simulation',
|
|
1585
|
+
request: {
|
|
1586
|
+
to: this.rollupContract.address,
|
|
1587
|
+
data: rollupData
|
|
1588
|
+
},
|
|
1589
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1590
|
+
error: {
|
|
1591
|
+
message: viemError.message,
|
|
1592
|
+
name: viemError.name
|
|
1593
|
+
},
|
|
1594
|
+
context: {
|
|
1595
|
+
actions: [
|
|
1596
|
+
'propose'
|
|
1597
|
+
],
|
|
1598
|
+
slot: Number(args[0].header.slotNumber),
|
|
1599
|
+
sender: this.getSenderAddress().toString()
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1302
1602
|
throw err;
|
|
1303
1603
|
});
|
|
1304
1604
|
return {
|