@aztec/sequencer-client 0.0.1-commit.29c6b1a3 → 0.0.1-commit.2d9cb6034
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 +15 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +60 -26
- package/dest/config.d.ts +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +47 -28
- package/dest/global_variable_builder/global_builder.d.ts +15 -11
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +29 -25
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -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 +76 -31
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +434 -88
- package/dest/sequencer/checkpoint_proposal_job.d.ts +61 -13
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +400 -201
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +21 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +122 -30
- package/dest/sequencer/sequencer.d.ts +42 -17
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +147 -89
- 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 +12 -12
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -36
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +76 -23
- package/src/config.ts +65 -38
- package/src/global_variable_builder/global_builder.ts +38 -27
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +153 -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 +474 -111
- package/src/sequencer/checkpoint_proposal_job.ts +532 -224
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +138 -32
- package/src/sequencer/sequencer.ts +213 -106
- 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 +65 -53
- package/src/test/utils.ts +5 -2
|
@@ -372,24 +372,30 @@ 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';
|
|
389
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
386
390
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
387
391
|
import { Timer } from '@aztec/foundation/timer';
|
|
388
392
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
389
393
|
import { encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
390
394
|
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
395
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
391
396
|
import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
392
|
-
import { encodeFunctionData, toHex } from 'viem';
|
|
397
|
+
import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
|
|
398
|
+
import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
393
399
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
394
400
|
export const Actions = [
|
|
395
401
|
'invalidate-by-invalid-attestation',
|
|
@@ -429,19 +435,22 @@ export class SequencerPublisher {
|
|
|
429
435
|
interrupted;
|
|
430
436
|
metrics;
|
|
431
437
|
epochCache;
|
|
438
|
+
failedTxStore;
|
|
432
439
|
governanceLog;
|
|
433
440
|
slashingLog;
|
|
434
441
|
lastActions;
|
|
435
442
|
isPayloadEmptyCache;
|
|
443
|
+
payloadProposedCache;
|
|
436
444
|
log;
|
|
437
445
|
ethereumSlotDuration;
|
|
446
|
+
aztecSlotDuration;
|
|
447
|
+
/** Date provider for wall-clock time. */ dateProvider;
|
|
438
448
|
blobClient;
|
|
439
449
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
450
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
|
|
440
451
|
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
444
|
-
static PROPOSE_GAS_GUESS = 12_000_000n;
|
|
452
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
|
|
453
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */ interruptibleSleep;
|
|
445
454
|
// A CALL to a cold address is 2700 gas
|
|
446
455
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
447
456
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -460,16 +469,22 @@ export class SequencerPublisher {
|
|
|
460
469
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
461
470
|
this.lastActions = {};
|
|
462
471
|
this.isPayloadEmptyCache = new Map();
|
|
472
|
+
this.payloadProposedCache = new Set();
|
|
473
|
+
this.interruptibleSleep = new InterruptibleSleep();
|
|
463
474
|
this.requests = [];
|
|
464
475
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
465
476
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
477
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
478
|
+
this.dateProvider = deps.dateProvider;
|
|
466
479
|
this.epochCache = deps.epochCache;
|
|
467
480
|
this.lastActions = deps.lastActions;
|
|
468
481
|
this.blobClient = deps.blobClient;
|
|
482
|
+
this.dateProvider = deps.dateProvider;
|
|
469
483
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
470
484
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
471
485
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
472
486
|
this.l1TxUtils = deps.l1TxUtils;
|
|
487
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
473
488
|
this.rollupContract = deps.rollupContract;
|
|
474
489
|
this.govProposerContract = deps.governanceProposerContract;
|
|
475
490
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -483,10 +498,36 @@ export class SequencerPublisher {
|
|
|
483
498
|
if (config.fishermanMode) {
|
|
484
499
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
485
500
|
}
|
|
501
|
+
// Initialize fee asset price oracle
|
|
502
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
|
|
503
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
504
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
508
|
+
* Does nothing if no store is configured.
|
|
509
|
+
*/ backupFailedTx(failedTx) {
|
|
510
|
+
if (!this.failedTxStore) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const tx = {
|
|
514
|
+
...failedTx,
|
|
515
|
+
timestamp: Date.now()
|
|
516
|
+
};
|
|
517
|
+
// Fire and forget - don't block on backup
|
|
518
|
+
void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
|
|
519
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
520
|
+
});
|
|
486
521
|
}
|
|
487
522
|
getRollupContract() {
|
|
488
523
|
return this.rollupContract;
|
|
489
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Gets the fee asset price modifier from the oracle.
|
|
527
|
+
* Returns 0n if the oracle query fails.
|
|
528
|
+
*/ getFeeAssetPriceModifier() {
|
|
529
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
530
|
+
}
|
|
490
531
|
getSenderAddress() {
|
|
491
532
|
return this.l1TxUtils.getSenderAddress();
|
|
492
533
|
}
|
|
@@ -505,7 +546,7 @@ export class SequencerPublisher {
|
|
|
505
546
|
this.requests.push(request);
|
|
506
547
|
}
|
|
507
548
|
getCurrentL2Slot() {
|
|
508
|
-
return this.epochCache.
|
|
549
|
+
return this.epochCache.getSlotNow();
|
|
509
550
|
}
|
|
510
551
|
/**
|
|
511
552
|
* Clears all pending requests without sending them.
|
|
@@ -544,7 +585,7 @@ export class SequencerPublisher {
|
|
|
544
585
|
// Get the transaction requests
|
|
545
586
|
const l1Requests = requestsToAnalyze.map((r)=>r.request);
|
|
546
587
|
// Start the analysis
|
|
547
|
-
const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit :
|
|
588
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
|
|
548
589
|
this.log.info('Started L1 fee analysis', {
|
|
549
590
|
analysisId,
|
|
550
591
|
l2SlotNumber: l2SlotNumber.toString(),
|
|
@@ -594,15 +635,24 @@ export class SequencerPublisher {
|
|
|
594
635
|
// @note - we can only have one blob config per bundle
|
|
595
636
|
// find requests with gas and blob configs
|
|
596
637
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
597
|
-
const gasConfigs =
|
|
598
|
-
const blobConfigs =
|
|
638
|
+
const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
|
|
639
|
+
const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
|
|
599
640
|
if (blobConfigs.length > 1) {
|
|
600
641
|
throw new Error('Multiple blob configs found');
|
|
601
642
|
}
|
|
602
643
|
const blobConfig = blobConfigs[0];
|
|
603
644
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
604
645
|
const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
|
|
605
|
-
|
|
646
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
647
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
648
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
649
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
650
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
651
|
+
requested: gasLimit,
|
|
652
|
+
capped: maxGas
|
|
653
|
+
});
|
|
654
|
+
gasLimit = maxGas;
|
|
655
|
+
}
|
|
606
656
|
const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
|
|
607
657
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
|
|
608
658
|
const txConfig = {
|
|
@@ -613,12 +663,34 @@ export class SequencerPublisher {
|
|
|
613
663
|
// This ensures the committee gets precomputed correctly
|
|
614
664
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
615
665
|
try {
|
|
666
|
+
// Capture context for failed tx backup before sending
|
|
667
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
668
|
+
const multicallData = encodeFunctionData({
|
|
669
|
+
abi: multicall3Abi,
|
|
670
|
+
functionName: 'aggregate3',
|
|
671
|
+
args: [
|
|
672
|
+
validRequests.map((r)=>({
|
|
673
|
+
target: r.request.to,
|
|
674
|
+
callData: r.request.data,
|
|
675
|
+
allowFailure: true
|
|
676
|
+
}))
|
|
677
|
+
]
|
|
678
|
+
});
|
|
679
|
+
const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
|
|
680
|
+
const txContext = {
|
|
681
|
+
multicallData,
|
|
682
|
+
blobData: blobDataHex,
|
|
683
|
+
l1BlockNumber
|
|
684
|
+
};
|
|
616
685
|
this.log.debug('Forwarding transactions', {
|
|
617
686
|
validRequests: validRequests.map((request)=>request.action),
|
|
618
687
|
txConfig
|
|
619
688
|
});
|
|
620
|
-
const result = await
|
|
621
|
-
|
|
689
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
690
|
+
if (result === undefined) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
|
|
622
694
|
return {
|
|
623
695
|
result,
|
|
624
696
|
expiredActions,
|
|
@@ -638,17 +710,100 @@ export class SequencerPublisher {
|
|
|
638
710
|
}
|
|
639
711
|
}
|
|
640
712
|
}
|
|
641
|
-
|
|
713
|
+
/**
|
|
714
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
715
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
716
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
717
|
+
*/ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
|
|
718
|
+
const triedAddresses = [];
|
|
719
|
+
let currentPublisher = this.l1TxUtils;
|
|
720
|
+
while(true){
|
|
721
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
722
|
+
try {
|
|
723
|
+
const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
|
|
724
|
+
this.l1TxUtils = currentPublisher;
|
|
725
|
+
return result;
|
|
726
|
+
} catch (err) {
|
|
727
|
+
if (err instanceof TimeoutError) {
|
|
728
|
+
throw err;
|
|
729
|
+
}
|
|
730
|
+
const viemError = formatViemError(err);
|
|
731
|
+
if (!this.getNextPublisher) {
|
|
732
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
|
|
736
|
+
const nextPublisher = await this.getNextPublisher([
|
|
737
|
+
...triedAddresses
|
|
738
|
+
]);
|
|
739
|
+
if (!nextPublisher) {
|
|
740
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
currentPublisher = nextPublisher;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/*
|
|
748
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
749
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
750
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
751
|
+
*/ async sendRequestsAt(submitAfter) {
|
|
752
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
753
|
+
if (ms > 0) {
|
|
754
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, {
|
|
755
|
+
submitAfter
|
|
756
|
+
});
|
|
757
|
+
await this.interruptibleSleep.sleep(ms);
|
|
758
|
+
}
|
|
759
|
+
if (this.interrupted) {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
return this.sendRequests();
|
|
763
|
+
}
|
|
764
|
+
callbackBundledTransactions(requests, result, txContext) {
|
|
642
765
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
643
766
|
if (result instanceof FormattedViemError) {
|
|
644
767
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
768
|
+
this.backupFailedTx({
|
|
769
|
+
id: keccak256(txContext.multicallData),
|
|
770
|
+
failureType: 'send-error',
|
|
771
|
+
request: {
|
|
772
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
773
|
+
data: txContext.multicallData
|
|
774
|
+
},
|
|
775
|
+
blobData: txContext.blobData,
|
|
776
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
777
|
+
error: {
|
|
778
|
+
message: result.message,
|
|
779
|
+
name: result.name
|
|
780
|
+
},
|
|
781
|
+
context: {
|
|
782
|
+
actions: requests.map((r)=>r.action),
|
|
783
|
+
requests: requests.map((r)=>({
|
|
784
|
+
action: r.action,
|
|
785
|
+
to: r.request.to,
|
|
786
|
+
data: r.request.data
|
|
787
|
+
})),
|
|
788
|
+
sender: this.getSenderAddress().toString()
|
|
789
|
+
}
|
|
790
|
+
});
|
|
645
791
|
return {
|
|
646
792
|
failedActions: requests.map((r)=>r.action)
|
|
647
793
|
};
|
|
648
794
|
} else {
|
|
649
795
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
650
796
|
result,
|
|
651
|
-
requests
|
|
797
|
+
requests: requests.map((r)=>({
|
|
798
|
+
...r,
|
|
799
|
+
// Avoid logging large blob data
|
|
800
|
+
blobConfig: r.blobConfig ? {
|
|
801
|
+
...r.blobConfig,
|
|
802
|
+
blobs: r.blobConfig.blobs.map((b)=>({
|
|
803
|
+
size: trimmedBytesLength(b)
|
|
804
|
+
}))
|
|
805
|
+
} : undefined
|
|
806
|
+
}))
|
|
652
807
|
});
|
|
653
808
|
const successfulActions = [];
|
|
654
809
|
const failedActions = [];
|
|
@@ -659,6 +814,37 @@ export class SequencerPublisher {
|
|
|
659
814
|
failedActions.push(request.action);
|
|
660
815
|
}
|
|
661
816
|
}
|
|
817
|
+
// Single backup for the whole reverted tx
|
|
818
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
819
|
+
this.backupFailedTx({
|
|
820
|
+
id: result.receipt.transactionHash,
|
|
821
|
+
failureType: 'revert',
|
|
822
|
+
request: {
|
|
823
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
824
|
+
data: txContext.multicallData
|
|
825
|
+
},
|
|
826
|
+
blobData: txContext.blobData,
|
|
827
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
828
|
+
receipt: {
|
|
829
|
+
transactionHash: result.receipt.transactionHash,
|
|
830
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
831
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
832
|
+
status: 'reverted'
|
|
833
|
+
},
|
|
834
|
+
error: {
|
|
835
|
+
message: result.errorMsg ?? 'Transaction reverted'
|
|
836
|
+
},
|
|
837
|
+
context: {
|
|
838
|
+
actions: failedActions,
|
|
839
|
+
requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
|
|
840
|
+
action: r.action,
|
|
841
|
+
to: r.request.to,
|
|
842
|
+
data: r.request.data
|
|
843
|
+
})),
|
|
844
|
+
sender: this.getSenderAddress().toString()
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
662
848
|
return {
|
|
663
849
|
successfulActions,
|
|
664
850
|
failedActions
|
|
@@ -666,18 +852,22 @@ export class SequencerPublisher {
|
|
|
666
852
|
}
|
|
667
853
|
}
|
|
668
854
|
/**
|
|
669
|
-
* @notice Will call `
|
|
855
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
670
856
|
* @param tipArchive - The archive to check
|
|
671
857
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
672
|
-
*/
|
|
858
|
+
*/ canProposeAt(tipArchive, msgSender, opts = {}) {
|
|
673
859
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
674
860
|
const ignoredErrors = [
|
|
675
861
|
'SlotAlreadyInChain',
|
|
676
862
|
'InvalidProposer',
|
|
677
863
|
'InvalidArchive'
|
|
678
864
|
];
|
|
679
|
-
|
|
680
|
-
|
|
865
|
+
const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
|
|
866
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
867
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
868
|
+
return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
869
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
870
|
+
forceArchive: opts.forceArchive
|
|
681
871
|
}).catch((err)=>{
|
|
682
872
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
683
873
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
@@ -708,7 +898,7 @@ export class SequencerPublisher {
|
|
|
708
898
|
header.blobsHash.toString(),
|
|
709
899
|
flags
|
|
710
900
|
];
|
|
711
|
-
const ts =
|
|
901
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
712
902
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
|
|
713
903
|
let balance = 0n;
|
|
714
904
|
if (this.config.fishermanMode) {
|
|
@@ -731,7 +921,7 @@ export class SequencerPublisher {
|
|
|
731
921
|
}),
|
|
732
922
|
from: MULTI_CALL_3_ADDRESS
|
|
733
923
|
}, {
|
|
734
|
-
time: ts
|
|
924
|
+
time: ts
|
|
735
925
|
}, stateOverrides);
|
|
736
926
|
this.log.debug(`Simulated validateHeader`);
|
|
737
927
|
}
|
|
@@ -761,8 +951,12 @@ export class SequencerPublisher {
|
|
|
761
951
|
...logData,
|
|
762
952
|
request
|
|
763
953
|
});
|
|
954
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
764
955
|
try {
|
|
765
|
-
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined,
|
|
956
|
+
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
957
|
+
request.abi ?? [],
|
|
958
|
+
ErrorsAbi
|
|
959
|
+
]));
|
|
766
960
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
767
961
|
...logData,
|
|
768
962
|
request,
|
|
@@ -773,20 +967,21 @@ export class SequencerPublisher {
|
|
|
773
967
|
gasUsed,
|
|
774
968
|
checkpointNumber,
|
|
775
969
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
970
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
776
971
|
reason
|
|
777
972
|
};
|
|
778
973
|
} catch (err) {
|
|
779
974
|
const viemError = formatViemError(err);
|
|
780
975
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
781
976
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
782
|
-
if (viemError.message?.includes('
|
|
977
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
783
978
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
|
|
784
979
|
...logData,
|
|
785
980
|
request,
|
|
786
981
|
error: viemError.message
|
|
787
982
|
});
|
|
788
|
-
const
|
|
789
|
-
if (
|
|
983
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
984
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
790
985
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
|
|
791
986
|
...logData
|
|
792
987
|
});
|
|
@@ -800,6 +995,27 @@ export class SequencerPublisher {
|
|
|
800
995
|
}
|
|
801
996
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
802
997
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
998
|
+
this.backupFailedTx({
|
|
999
|
+
id: keccak256(request.data),
|
|
1000
|
+
failureType: 'simulation',
|
|
1001
|
+
request: {
|
|
1002
|
+
to: request.to,
|
|
1003
|
+
data: request.data,
|
|
1004
|
+
value: request.value?.toString()
|
|
1005
|
+
},
|
|
1006
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1007
|
+
error: {
|
|
1008
|
+
message: viemError.message,
|
|
1009
|
+
name: viemError.name
|
|
1010
|
+
},
|
|
1011
|
+
context: {
|
|
1012
|
+
actions: [
|
|
1013
|
+
`invalidate-${reason}`
|
|
1014
|
+
],
|
|
1015
|
+
checkpointNumber,
|
|
1016
|
+
sender: this.getSenderAddress().toString()
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
803
1019
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
804
1020
|
cause: viemError
|
|
805
1021
|
});
|
|
@@ -826,30 +1042,15 @@ export class SequencerPublisher {
|
|
|
826
1042
|
}
|
|
827
1043
|
}
|
|
828
1044
|
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
|
|
829
|
-
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
1045
|
const blobFields = checkpoint.toBlobFields();
|
|
845
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1046
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
846
1047
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
847
1048
|
const args = [
|
|
848
1049
|
{
|
|
849
1050
|
header: checkpoint.header.toViem(),
|
|
850
1051
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
851
1052
|
oracleInput: {
|
|
852
|
-
feeAssetPriceModifier:
|
|
1053
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
853
1054
|
}
|
|
854
1055
|
},
|
|
855
1056
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -857,10 +1058,9 @@ export class SequencerPublisher {
|
|
|
857
1058
|
attestationsAndSignersSignature.toViemSignature(),
|
|
858
1059
|
blobInput
|
|
859
1060
|
];
|
|
860
|
-
await this.simulateProposeTx(args,
|
|
861
|
-
return ts;
|
|
1061
|
+
await this.simulateProposeTx(args, options);
|
|
862
1062
|
}
|
|
863
|
-
async enqueueCastSignalHelper(slotNumber,
|
|
1063
|
+
async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
|
|
864
1064
|
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
865
1065
|
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
866
1066
|
return false;
|
|
@@ -884,6 +1084,28 @@ export class SequencerPublisher {
|
|
|
884
1084
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
885
1085
|
return false;
|
|
886
1086
|
}
|
|
1087
|
+
// Check if payload was already submitted to governance
|
|
1088
|
+
const cacheKey = payload.toString();
|
|
1089
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
1090
|
+
try {
|
|
1091
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
1092
|
+
const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
|
|
1093
|
+
0,
|
|
1094
|
+
1,
|
|
1095
|
+
2
|
|
1096
|
+
]), this.log, true);
|
|
1097
|
+
if (proposed) {
|
|
1098
|
+
this.payloadProposedCache.add(cacheKey);
|
|
1099
|
+
}
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
1106
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
887
1109
|
const cachedLastVote = this.lastActions[signalType];
|
|
888
1110
|
this.lastActions[signalType] = slotNumber;
|
|
889
1111
|
const action = signalType;
|
|
@@ -894,15 +1116,45 @@ export class SequencerPublisher {
|
|
|
894
1116
|
signer: this.l1TxUtils.client.account?.address,
|
|
895
1117
|
lastValidL2Slot: slotNumber
|
|
896
1118
|
});
|
|
1119
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1120
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
897
1121
|
try {
|
|
898
1122
|
await this.l1TxUtils.simulate(request, {
|
|
899
1123
|
time: timestamp
|
|
900
|
-
}, [],
|
|
1124
|
+
}, [], mergeAbis([
|
|
1125
|
+
request.abi ?? [],
|
|
1126
|
+
ErrorsAbi
|
|
1127
|
+
]));
|
|
901
1128
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
|
|
902
1129
|
request
|
|
903
1130
|
});
|
|
904
1131
|
} catch (err) {
|
|
905
|
-
|
|
1132
|
+
const viemError = formatViemError(err);
|
|
1133
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
1134
|
+
simulationTimestamp: timestamp,
|
|
1135
|
+
l1BlockNumber
|
|
1136
|
+
});
|
|
1137
|
+
this.backupFailedTx({
|
|
1138
|
+
id: keccak256(request.data),
|
|
1139
|
+
failureType: 'simulation',
|
|
1140
|
+
request: {
|
|
1141
|
+
to: request.to,
|
|
1142
|
+
data: request.data,
|
|
1143
|
+
value: request.value?.toString()
|
|
1144
|
+
},
|
|
1145
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1146
|
+
error: {
|
|
1147
|
+
message: viemError.message,
|
|
1148
|
+
name: viemError.name
|
|
1149
|
+
},
|
|
1150
|
+
context: {
|
|
1151
|
+
actions: [
|
|
1152
|
+
action
|
|
1153
|
+
],
|
|
1154
|
+
slot: slotNumber,
|
|
1155
|
+
sender: this.getSenderAddress().toString()
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
906
1158
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
907
1159
|
}
|
|
908
1160
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -946,12 +1198,11 @@ export class SequencerPublisher {
|
|
|
946
1198
|
/**
|
|
947
1199
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
948
1200
|
* @param slotNumber - The slot number to cast a signal for.
|
|
949
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
950
1201
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
951
|
-
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber,
|
|
952
|
-
return this.enqueueCastSignalHelper(slotNumber,
|
|
1202
|
+
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
|
|
1203
|
+
return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
953
1204
|
}
|
|
954
|
-
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber,
|
|
1205
|
+
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
|
|
955
1206
|
if (actions.length === 0) {
|
|
956
1207
|
this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
|
|
957
1208
|
return false;
|
|
@@ -967,7 +1218,7 @@ export class SequencerPublisher {
|
|
|
967
1218
|
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
968
1219
|
signerAddress
|
|
969
1220
|
});
|
|
970
|
-
await this.enqueueCastSignalHelper(slotNumber,
|
|
1221
|
+
await this.enqueueCastSignalHelper(slotNumber, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
|
|
971
1222
|
break;
|
|
972
1223
|
}
|
|
973
1224
|
case 'create-empire-payload':
|
|
@@ -977,7 +1228,7 @@ export class SequencerPublisher {
|
|
|
977
1228
|
signerAddress
|
|
978
1229
|
});
|
|
979
1230
|
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
980
|
-
await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber
|
|
1231
|
+
await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber);
|
|
981
1232
|
break;
|
|
982
1233
|
}
|
|
983
1234
|
case 'execute-empire-payload':
|
|
@@ -992,7 +1243,7 @@ export class SequencerPublisher {
|
|
|
992
1243
|
}
|
|
993
1244
|
const empireSlashingProposer = this.slashingProposerContract;
|
|
994
1245
|
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
995
|
-
await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber
|
|
1246
|
+
await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber);
|
|
996
1247
|
break;
|
|
997
1248
|
}
|
|
998
1249
|
case 'vote-offenses':
|
|
@@ -1010,7 +1261,7 @@ export class SequencerPublisher {
|
|
|
1010
1261
|
const tallySlashingProposer = this.slashingProposerContract;
|
|
1011
1262
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1012
1263
|
const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1013
|
-
await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber
|
|
1264
|
+
await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber);
|
|
1014
1265
|
break;
|
|
1015
1266
|
}
|
|
1016
1267
|
case 'execute-slash':
|
|
@@ -1026,7 +1277,7 @@ export class SequencerPublisher {
|
|
|
1026
1277
|
}
|
|
1027
1278
|
const tallySlashingProposer = this.slashingProposerContract;
|
|
1028
1279
|
const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
|
|
1029
|
-
await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber
|
|
1280
|
+
await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
|
|
1030
1281
|
break;
|
|
1031
1282
|
}
|
|
1032
1283
|
default:
|
|
@@ -1041,22 +1292,22 @@ export class SequencerPublisher {
|
|
|
1041
1292
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1042
1293
|
const checkpointHeader = checkpoint.header;
|
|
1043
1294
|
const blobFields = checkpoint.toBlobFields();
|
|
1044
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1295
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1045
1296
|
const proposeTxArgs = {
|
|
1046
1297
|
header: checkpointHeader,
|
|
1047
1298
|
archive: checkpoint.archive.root.toBuffer(),
|
|
1048
1299
|
blobs,
|
|
1049
1300
|
attestationsAndSigners,
|
|
1050
|
-
attestationsAndSignersSignature
|
|
1301
|
+
attestationsAndSignersSignature,
|
|
1302
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1051
1303
|
};
|
|
1052
|
-
let ts;
|
|
1053
1304
|
try {
|
|
1054
1305
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
1055
1306
|
// This means that we can avoid the simulation issues in later checks.
|
|
1056
1307
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1057
1308
|
// make time consistency checks break.
|
|
1058
1309
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1059
|
-
|
|
1310
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
|
|
1060
1311
|
} catch (err) {
|
|
1061
1312
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1062
1313
|
...checkpoint.getStats(),
|
|
@@ -1069,7 +1320,7 @@ export class SequencerPublisher {
|
|
|
1069
1320
|
...checkpoint.toCheckpointInfo(),
|
|
1070
1321
|
...opts
|
|
1071
1322
|
});
|
|
1072
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts
|
|
1323
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts);
|
|
1073
1324
|
}
|
|
1074
1325
|
enqueueInvalidateCheckpoint(request, opts = {}) {
|
|
1075
1326
|
if (!request) {
|
|
@@ -1110,7 +1361,8 @@ export class SequencerPublisher {
|
|
|
1110
1361
|
}
|
|
1111
1362
|
});
|
|
1112
1363
|
}
|
|
1113
|
-
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber
|
|
1364
|
+
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
|
|
1365
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1114
1366
|
const logData = {
|
|
1115
1367
|
slotNumber,
|
|
1116
1368
|
timestamp,
|
|
@@ -1123,28 +1375,60 @@ export class SequencerPublisher {
|
|
|
1123
1375
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1124
1376
|
this.lastActions[action] = slotNumber;
|
|
1125
1377
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1378
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1126
1379
|
let gasUsed;
|
|
1380
|
+
const simulateAbi = mergeAbis([
|
|
1381
|
+
request.abi ?? [],
|
|
1382
|
+
ErrorsAbi
|
|
1383
|
+
]);
|
|
1127
1384
|
try {
|
|
1128
1385
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1129
1386
|
time: timestamp
|
|
1130
|
-
}, [],
|
|
1387
|
+
}, [], simulateAbi));
|
|
1131
1388
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1132
1389
|
...logData,
|
|
1133
1390
|
request,
|
|
1134
1391
|
gasUsed
|
|
1135
1392
|
});
|
|
1136
1393
|
} catch (err) {
|
|
1137
|
-
const viemError = formatViemError(err);
|
|
1394
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1138
1395
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1396
|
+
this.backupFailedTx({
|
|
1397
|
+
id: keccak256(request.data),
|
|
1398
|
+
failureType: 'simulation',
|
|
1399
|
+
request: {
|
|
1400
|
+
to: request.to,
|
|
1401
|
+
data: request.data,
|
|
1402
|
+
value: request.value?.toString()
|
|
1403
|
+
},
|
|
1404
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1405
|
+
error: {
|
|
1406
|
+
message: viemError.message,
|
|
1407
|
+
name: viemError.name
|
|
1408
|
+
},
|
|
1409
|
+
context: {
|
|
1410
|
+
actions: [
|
|
1411
|
+
action
|
|
1412
|
+
],
|
|
1413
|
+
slot: slotNumber,
|
|
1414
|
+
sender: this.getSenderAddress().toString()
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1139
1417
|
return false;
|
|
1140
1418
|
}
|
|
1141
1419
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1142
1420
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
|
|
1143
1421
|
logData.gasLimit = gasLimit;
|
|
1422
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1423
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1424
|
+
const requestWithAbi = {
|
|
1425
|
+
...request,
|
|
1426
|
+
abi: simulateAbi
|
|
1427
|
+
};
|
|
1144
1428
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1145
1429
|
this.addRequest({
|
|
1146
1430
|
action,
|
|
1147
|
-
request,
|
|
1431
|
+
request: requestWithAbi,
|
|
1148
1432
|
gasConfig: {
|
|
1149
1433
|
gasLimit
|
|
1150
1434
|
},
|
|
@@ -1175,13 +1459,14 @@ export class SequencerPublisher {
|
|
|
1175
1459
|
* A call to `restart` is required before you can continue publishing.
|
|
1176
1460
|
*/ interrupt() {
|
|
1177
1461
|
this.interrupted = true;
|
|
1462
|
+
this.interruptibleSleep.interrupt();
|
|
1178
1463
|
this.l1TxUtils.interrupt();
|
|
1179
1464
|
}
|
|
1180
1465
|
/** Restarts the publisher after calling `interrupt`. */ restart() {
|
|
1181
1466
|
this.interrupted = false;
|
|
1182
1467
|
this.l1TxUtils.restart();
|
|
1183
1468
|
}
|
|
1184
|
-
async prepareProposeTx(encodedData,
|
|
1469
|
+
async prepareProposeTx(encodedData, options) {
|
|
1185
1470
|
const kzg = Blob.getViemKzgInstance();
|
|
1186
1471
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1187
1472
|
this.log.debug('Validating blob input', {
|
|
@@ -1208,10 +1493,38 @@ export class SequencerPublisher {
|
|
|
1208
1493
|
}, {}, {
|
|
1209
1494
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1210
1495
|
kzg
|
|
1211
|
-
}).catch((err)=>{
|
|
1212
|
-
const
|
|
1213
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1214
|
-
metaMessages
|
|
1496
|
+
}).catch(async (err)=>{
|
|
1497
|
+
const viemError = formatViemError(err);
|
|
1498
|
+
this.log.error(`Failed to validate blobs`, viemError.message, {
|
|
1499
|
+
metaMessages: viemError.metaMessages
|
|
1500
|
+
});
|
|
1501
|
+
const validateBlobsData = encodeFunctionData({
|
|
1502
|
+
abi: RollupAbi,
|
|
1503
|
+
functionName: 'validateBlobs',
|
|
1504
|
+
args: [
|
|
1505
|
+
blobInput
|
|
1506
|
+
]
|
|
1507
|
+
});
|
|
1508
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1509
|
+
this.backupFailedTx({
|
|
1510
|
+
id: keccak256(validateBlobsData),
|
|
1511
|
+
failureType: 'simulation',
|
|
1512
|
+
request: {
|
|
1513
|
+
to: this.rollupContract.address,
|
|
1514
|
+
data: validateBlobsData
|
|
1515
|
+
},
|
|
1516
|
+
blobData: encodedData.blobs.map((b)=>toHex(b.data)),
|
|
1517
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1518
|
+
error: {
|
|
1519
|
+
message: viemError.message,
|
|
1520
|
+
name: viemError.name
|
|
1521
|
+
},
|
|
1522
|
+
context: {
|
|
1523
|
+
actions: [
|
|
1524
|
+
'validate-blobs'
|
|
1525
|
+
],
|
|
1526
|
+
sender: this.getSenderAddress().toString()
|
|
1527
|
+
}
|
|
1215
1528
|
});
|
|
1216
1529
|
throw new Error('Failed to validate blobs');
|
|
1217
1530
|
});
|
|
@@ -1222,8 +1535,7 @@ export class SequencerPublisher {
|
|
|
1222
1535
|
header: encodedData.header.toViem(),
|
|
1223
1536
|
archive: toHex(encodedData.archive),
|
|
1224
1537
|
oracleInput: {
|
|
1225
|
-
|
|
1226
|
-
feeAssetPriceModifier: 0n
|
|
1538
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier
|
|
1227
1539
|
}
|
|
1228
1540
|
},
|
|
1229
1541
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1231,7 +1543,7 @@ export class SequencerPublisher {
|
|
|
1231
1543
|
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
1232
1544
|
blobInput
|
|
1233
1545
|
];
|
|
1234
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1546
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
|
|
1235
1547
|
return {
|
|
1236
1548
|
args,
|
|
1237
1549
|
blobEvaluationGas,
|
|
@@ -1242,16 +1554,17 @@ export class SequencerPublisher {
|
|
|
1242
1554
|
/**
|
|
1243
1555
|
* Simulates the propose tx with eth_simulateV1
|
|
1244
1556
|
* @param args - The propose tx args
|
|
1245
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1246
1557
|
* @returns The simulation result
|
|
1247
|
-
*/ async simulateProposeTx(args,
|
|
1558
|
+
*/ async simulateProposeTx(args, options) {
|
|
1248
1559
|
const rollupData = encodeFunctionData({
|
|
1249
1560
|
abi: RollupAbi,
|
|
1250
1561
|
functionName: 'propose',
|
|
1251
1562
|
args
|
|
1252
1563
|
});
|
|
1253
|
-
// override the
|
|
1564
|
+
// override the proposed checkpoint number if requested
|
|
1254
1565
|
const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1566
|
+
// override the fee header for a specific checkpoint number if requested (used when pipelining)
|
|
1567
|
+
const forceProposedFeeHeaderStateDiff = (options.forceProposedFeeHeader !== undefined ? await this.rollupContract.makeFeeHeaderOverride(options.forceProposedFeeHeader.checkpointNumber, options.forceProposedFeeHeader.feeHeader) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1255
1568
|
const stateOverrides = [
|
|
1256
1569
|
{
|
|
1257
1570
|
address: this.rollupContract.address,
|
|
@@ -1261,7 +1574,8 @@ export class SequencerPublisher {
|
|
|
1261
1574
|
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
1262
1575
|
value: toPaddedHex(0n, true)
|
|
1263
1576
|
},
|
|
1264
|
-
...forcePendingCheckpointNumberStateDiff
|
|
1577
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1578
|
+
...forceProposedFeeHeaderStateDiff
|
|
1265
1579
|
]
|
|
1266
1580
|
}
|
|
1267
1581
|
];
|
|
@@ -1272,21 +1586,22 @@ export class SequencerPublisher {
|
|
|
1272
1586
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1273
1587
|
});
|
|
1274
1588
|
}
|
|
1589
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1590
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1275
1591
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1276
1592
|
to: this.rollupContract.address,
|
|
1277
1593
|
data: rollupData,
|
|
1278
|
-
gas:
|
|
1594
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1279
1595
|
...this.proposerAddressForSimulation && {
|
|
1280
1596
|
from: this.proposerAddressForSimulation.toString()
|
|
1281
1597
|
}
|
|
1282
1598
|
}, {
|
|
1283
|
-
|
|
1284
|
-
time: timestamp + 1n,
|
|
1599
|
+
time: simTs,
|
|
1285
1600
|
// @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:
|
|
1601
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1287
1602
|
}, stateOverrides, RollupAbi, {
|
|
1288
1603
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1289
|
-
fallbackGasEstimate:
|
|
1604
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT
|
|
1290
1605
|
}).catch((err)=>{
|
|
1291
1606
|
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1292
1607
|
const viemError = formatViemError(err);
|
|
@@ -1294,11 +1609,33 @@ export class SequencerPublisher {
|
|
|
1294
1609
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1295
1610
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1296
1611
|
return {
|
|
1297
|
-
gasUsed:
|
|
1612
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1298
1613
|
logs: []
|
|
1299
1614
|
};
|
|
1300
1615
|
}
|
|
1301
|
-
this.log.error(`Failed to simulate propose tx`, viemError
|
|
1616
|
+
this.log.error(`Failed to simulate propose tx`, viemError, {
|
|
1617
|
+
simulationTimestamp: simTs
|
|
1618
|
+
});
|
|
1619
|
+
this.backupFailedTx({
|
|
1620
|
+
id: keccak256(rollupData),
|
|
1621
|
+
failureType: 'simulation',
|
|
1622
|
+
request: {
|
|
1623
|
+
to: this.rollupContract.address,
|
|
1624
|
+
data: rollupData
|
|
1625
|
+
},
|
|
1626
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1627
|
+
error: {
|
|
1628
|
+
message: viemError.message,
|
|
1629
|
+
name: viemError.name
|
|
1630
|
+
},
|
|
1631
|
+
context: {
|
|
1632
|
+
actions: [
|
|
1633
|
+
'propose'
|
|
1634
|
+
],
|
|
1635
|
+
slot: Number(args[0].header.slotNumber),
|
|
1636
|
+
sender: this.getSenderAddress().toString()
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1302
1639
|
throw err;
|
|
1303
1640
|
});
|
|
1304
1641
|
return {
|
|
@@ -1306,11 +1643,11 @@ export class SequencerPublisher {
|
|
|
1306
1643
|
simulationResult
|
|
1307
1644
|
};
|
|
1308
1645
|
}
|
|
1309
|
-
async addProposeTx(checkpoint, encodedData, opts = {}
|
|
1646
|
+
async addProposeTx(checkpoint, encodedData, opts = {}) {
|
|
1310
1647
|
const slot = checkpoint.header.slotNumber;
|
|
1311
1648
|
const timer = new Timer();
|
|
1312
1649
|
const kzg = Blob.getViemKzgInstance();
|
|
1313
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData,
|
|
1650
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
|
|
1314
1651
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1315
1652
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
1316
1653
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
@@ -1375,4 +1712,13 @@ export class SequencerPublisher {
|
|
|
1375
1712
|
}
|
|
1376
1713
|
});
|
|
1377
1714
|
}
|
|
1715
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1716
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
|
|
1717
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1718
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1719
|
+
}
|
|
1720
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
|
|
1721
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1722
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1723
|
+
}
|
|
1378
1724
|
}
|