@aztec/sequencer-client 0.0.1-commit.f146247c → 0.0.1-commit.f1b29a41e
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.d.ts +76 -30
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +396 -71
- package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -8
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +368 -196
- 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 +97 -15
- 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 +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- 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.ts +442 -95
- package/src/sequencer/checkpoint_proposal_job.ts +481 -202
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +106 -18
- package/src/sequencer/sequencer.ts +212 -105
- 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 +63 -49
- 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
377
|
import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
|
|
378
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,15 +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;
|
|
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;
|
|
441
454
|
// A CALL to a cold address is 2700 gas
|
|
442
455
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
443
456
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -456,16 +469,22 @@ export class SequencerPublisher {
|
|
|
456
469
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
457
470
|
this.lastActions = {};
|
|
458
471
|
this.isPayloadEmptyCache = new Map();
|
|
472
|
+
this.payloadProposedCache = new Set();
|
|
473
|
+
this.interruptibleSleep = new InterruptibleSleep();
|
|
459
474
|
this.requests = [];
|
|
460
475
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
461
476
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
477
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
478
|
+
this.dateProvider = deps.dateProvider;
|
|
462
479
|
this.epochCache = deps.epochCache;
|
|
463
480
|
this.lastActions = deps.lastActions;
|
|
464
481
|
this.blobClient = deps.blobClient;
|
|
482
|
+
this.dateProvider = deps.dateProvider;
|
|
465
483
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
466
484
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
467
485
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
468
486
|
this.l1TxUtils = deps.l1TxUtils;
|
|
487
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
469
488
|
this.rollupContract = deps.rollupContract;
|
|
470
489
|
this.govProposerContract = deps.governanceProposerContract;
|
|
471
490
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -479,10 +498,36 @@ export class SequencerPublisher {
|
|
|
479
498
|
if (config.fishermanMode) {
|
|
480
499
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
481
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
|
+
});
|
|
482
521
|
}
|
|
483
522
|
getRollupContract() {
|
|
484
523
|
return this.rollupContract;
|
|
485
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
|
+
}
|
|
486
531
|
getSenderAddress() {
|
|
487
532
|
return this.l1TxUtils.getSenderAddress();
|
|
488
533
|
}
|
|
@@ -501,7 +546,7 @@ export class SequencerPublisher {
|
|
|
501
546
|
this.requests.push(request);
|
|
502
547
|
}
|
|
503
548
|
getCurrentL2Slot() {
|
|
504
|
-
return this.epochCache.
|
|
549
|
+
return this.epochCache.getSlotNow();
|
|
505
550
|
}
|
|
506
551
|
/**
|
|
507
552
|
* Clears all pending requests without sending them.
|
|
@@ -590,8 +635,8 @@ export class SequencerPublisher {
|
|
|
590
635
|
// @note - we can only have one blob config per bundle
|
|
591
636
|
// find requests with gas and blob configs
|
|
592
637
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
593
|
-
const gasConfigs =
|
|
594
|
-
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);
|
|
595
640
|
if (blobConfigs.length > 1) {
|
|
596
641
|
throw new Error('Multiple blob configs found');
|
|
597
642
|
}
|
|
@@ -618,12 +663,34 @@ export class SequencerPublisher {
|
|
|
618
663
|
// This ensures the committee gets precomputed correctly
|
|
619
664
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
620
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
|
+
};
|
|
621
685
|
this.log.debug('Forwarding transactions', {
|
|
622
686
|
validRequests: validRequests.map((request)=>request.action),
|
|
623
687
|
txConfig
|
|
624
688
|
});
|
|
625
|
-
const result = await
|
|
626
|
-
|
|
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);
|
|
627
694
|
return {
|
|
628
695
|
result,
|
|
629
696
|
expiredActions,
|
|
@@ -643,17 +710,100 @@ export class SequencerPublisher {
|
|
|
643
710
|
}
|
|
644
711
|
}
|
|
645
712
|
}
|
|
646
|
-
|
|
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) {
|
|
647
765
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
648
766
|
if (result instanceof FormattedViemError) {
|
|
649
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
|
+
});
|
|
650
791
|
return {
|
|
651
792
|
failedActions: requests.map((r)=>r.action)
|
|
652
793
|
};
|
|
653
794
|
} else {
|
|
654
795
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
655
796
|
result,
|
|
656
|
-
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
|
+
}))
|
|
657
807
|
});
|
|
658
808
|
const successfulActions = [];
|
|
659
809
|
const failedActions = [];
|
|
@@ -664,6 +814,37 @@ export class SequencerPublisher {
|
|
|
664
814
|
failedActions.push(request.action);
|
|
665
815
|
}
|
|
666
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
|
+
}
|
|
667
848
|
return {
|
|
668
849
|
successfulActions,
|
|
669
850
|
failedActions
|
|
@@ -671,18 +852,22 @@ export class SequencerPublisher {
|
|
|
671
852
|
}
|
|
672
853
|
}
|
|
673
854
|
/**
|
|
674
|
-
* @notice Will call `
|
|
855
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
675
856
|
* @param tipArchive - The archive to check
|
|
676
857
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
677
|
-
*/
|
|
858
|
+
*/ canProposeAt(tipArchive, msgSender, opts = {}) {
|
|
678
859
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
679
860
|
const ignoredErrors = [
|
|
680
861
|
'SlotAlreadyInChain',
|
|
681
862
|
'InvalidProposer',
|
|
682
863
|
'InvalidArchive'
|
|
683
864
|
];
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
686
871
|
}).catch((err)=>{
|
|
687
872
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
688
873
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
@@ -713,7 +898,7 @@ export class SequencerPublisher {
|
|
|
713
898
|
header.blobsHash.toString(),
|
|
714
899
|
flags
|
|
715
900
|
];
|
|
716
|
-
const ts =
|
|
901
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
717
902
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
|
|
718
903
|
let balance = 0n;
|
|
719
904
|
if (this.config.fishermanMode) {
|
|
@@ -736,7 +921,7 @@ export class SequencerPublisher {
|
|
|
736
921
|
}),
|
|
737
922
|
from: MULTI_CALL_3_ADDRESS
|
|
738
923
|
}, {
|
|
739
|
-
time: ts
|
|
924
|
+
time: ts
|
|
740
925
|
}, stateOverrides);
|
|
741
926
|
this.log.debug(`Simulated validateHeader`);
|
|
742
927
|
}
|
|
@@ -766,6 +951,7 @@ export class SequencerPublisher {
|
|
|
766
951
|
...logData,
|
|
767
952
|
request
|
|
768
953
|
});
|
|
954
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
769
955
|
try {
|
|
770
956
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
771
957
|
request.abi ?? [],
|
|
@@ -781,6 +967,7 @@ export class SequencerPublisher {
|
|
|
781
967
|
gasUsed,
|
|
782
968
|
checkpointNumber,
|
|
783
969
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
970
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
784
971
|
reason
|
|
785
972
|
};
|
|
786
973
|
} catch (err) {
|
|
@@ -793,8 +980,8 @@ export class SequencerPublisher {
|
|
|
793
980
|
request,
|
|
794
981
|
error: viemError.message
|
|
795
982
|
});
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
983
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
984
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
798
985
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
|
|
799
986
|
...logData
|
|
800
987
|
});
|
|
@@ -808,6 +995,27 @@ export class SequencerPublisher {
|
|
|
808
995
|
}
|
|
809
996
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
810
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
|
+
});
|
|
811
1019
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
812
1020
|
cause: viemError
|
|
813
1021
|
});
|
|
@@ -834,30 +1042,15 @@ export class SequencerPublisher {
|
|
|
834
1042
|
}
|
|
835
1043
|
}
|
|
836
1044
|
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
|
|
837
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
838
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
839
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
840
|
-
// so that the committee is recalculated correctly
|
|
841
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
842
|
-
// if (ignoreSignatures) {
|
|
843
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
844
|
-
// if (!committee) {
|
|
845
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
846
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
847
|
-
// }
|
|
848
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
849
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
850
|
-
// );
|
|
851
|
-
// }
|
|
852
1045
|
const blobFields = checkpoint.toBlobFields();
|
|
853
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1046
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
854
1047
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
855
1048
|
const args = [
|
|
856
1049
|
{
|
|
857
1050
|
header: checkpoint.header.toViem(),
|
|
858
1051
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
859
1052
|
oracleInput: {
|
|
860
|
-
feeAssetPriceModifier:
|
|
1053
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
861
1054
|
}
|
|
862
1055
|
},
|
|
863
1056
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -865,10 +1058,9 @@ export class SequencerPublisher {
|
|
|
865
1058
|
attestationsAndSignersSignature.toViemSignature(),
|
|
866
1059
|
blobInput
|
|
867
1060
|
];
|
|
868
|
-
await this.simulateProposeTx(args,
|
|
869
|
-
return ts;
|
|
1061
|
+
await this.simulateProposeTx(args, options);
|
|
870
1062
|
}
|
|
871
|
-
async enqueueCastSignalHelper(slotNumber,
|
|
1063
|
+
async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
|
|
872
1064
|
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
873
1065
|
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
874
1066
|
return false;
|
|
@@ -892,6 +1084,28 @@ export class SequencerPublisher {
|
|
|
892
1084
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
893
1085
|
return false;
|
|
894
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
|
+
}
|
|
895
1109
|
const cachedLastVote = this.lastActions[signalType];
|
|
896
1110
|
this.lastActions[signalType] = slotNumber;
|
|
897
1111
|
const action = signalType;
|
|
@@ -902,6 +1116,8 @@ export class SequencerPublisher {
|
|
|
902
1116
|
signer: this.l1TxUtils.client.account?.address,
|
|
903
1117
|
lastValidL2Slot: slotNumber
|
|
904
1118
|
});
|
|
1119
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1120
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
905
1121
|
try {
|
|
906
1122
|
await this.l1TxUtils.simulate(request, {
|
|
907
1123
|
time: timestamp
|
|
@@ -913,7 +1129,32 @@ export class SequencerPublisher {
|
|
|
913
1129
|
request
|
|
914
1130
|
});
|
|
915
1131
|
} catch (err) {
|
|
916
|
-
|
|
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
|
+
});
|
|
917
1158
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
918
1159
|
}
|
|
919
1160
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -957,12 +1198,11 @@ export class SequencerPublisher {
|
|
|
957
1198
|
/**
|
|
958
1199
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
959
1200
|
* @param slotNumber - The slot number to cast a signal for.
|
|
960
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
961
1201
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
962
|
-
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber,
|
|
963
|
-
return this.enqueueCastSignalHelper(slotNumber,
|
|
1202
|
+
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
|
|
1203
|
+
return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
964
1204
|
}
|
|
965
|
-
/** 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) {
|
|
966
1206
|
if (actions.length === 0) {
|
|
967
1207
|
this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
|
|
968
1208
|
return false;
|
|
@@ -978,7 +1218,7 @@ export class SequencerPublisher {
|
|
|
978
1218
|
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
979
1219
|
signerAddress
|
|
980
1220
|
});
|
|
981
|
-
await this.enqueueCastSignalHelper(slotNumber,
|
|
1221
|
+
await this.enqueueCastSignalHelper(slotNumber, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
|
|
982
1222
|
break;
|
|
983
1223
|
}
|
|
984
1224
|
case 'create-empire-payload':
|
|
@@ -988,7 +1228,7 @@ export class SequencerPublisher {
|
|
|
988
1228
|
signerAddress
|
|
989
1229
|
});
|
|
990
1230
|
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
991
|
-
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);
|
|
992
1232
|
break;
|
|
993
1233
|
}
|
|
994
1234
|
case 'execute-empire-payload':
|
|
@@ -1003,7 +1243,7 @@ export class SequencerPublisher {
|
|
|
1003
1243
|
}
|
|
1004
1244
|
const empireSlashingProposer = this.slashingProposerContract;
|
|
1005
1245
|
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1006
|
-
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);
|
|
1007
1247
|
break;
|
|
1008
1248
|
}
|
|
1009
1249
|
case 'vote-offenses':
|
|
@@ -1021,7 +1261,7 @@ export class SequencerPublisher {
|
|
|
1021
1261
|
const tallySlashingProposer = this.slashingProposerContract;
|
|
1022
1262
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1023
1263
|
const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1024
|
-
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);
|
|
1025
1265
|
break;
|
|
1026
1266
|
}
|
|
1027
1267
|
case 'execute-slash':
|
|
@@ -1037,7 +1277,7 @@ export class SequencerPublisher {
|
|
|
1037
1277
|
}
|
|
1038
1278
|
const tallySlashingProposer = this.slashingProposerContract;
|
|
1039
1279
|
const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
|
|
1040
|
-
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);
|
|
1041
1281
|
break;
|
|
1042
1282
|
}
|
|
1043
1283
|
default:
|
|
@@ -1052,22 +1292,22 @@ export class SequencerPublisher {
|
|
|
1052
1292
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1053
1293
|
const checkpointHeader = checkpoint.header;
|
|
1054
1294
|
const blobFields = checkpoint.toBlobFields();
|
|
1055
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1295
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1056
1296
|
const proposeTxArgs = {
|
|
1057
1297
|
header: checkpointHeader,
|
|
1058
1298
|
archive: checkpoint.archive.root.toBuffer(),
|
|
1059
1299
|
blobs,
|
|
1060
1300
|
attestationsAndSigners,
|
|
1061
|
-
attestationsAndSignersSignature
|
|
1301
|
+
attestationsAndSignersSignature,
|
|
1302
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1062
1303
|
};
|
|
1063
|
-
let ts;
|
|
1064
1304
|
try {
|
|
1065
1305
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
1066
1306
|
// This means that we can avoid the simulation issues in later checks.
|
|
1067
1307
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1068
1308
|
// make time consistency checks break.
|
|
1069
1309
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1070
|
-
|
|
1310
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
|
|
1071
1311
|
} catch (err) {
|
|
1072
1312
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1073
1313
|
...checkpoint.getStats(),
|
|
@@ -1080,7 +1320,7 @@ export class SequencerPublisher {
|
|
|
1080
1320
|
...checkpoint.toCheckpointInfo(),
|
|
1081
1321
|
...opts
|
|
1082
1322
|
});
|
|
1083
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts
|
|
1323
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts);
|
|
1084
1324
|
}
|
|
1085
1325
|
enqueueInvalidateCheckpoint(request, opts = {}) {
|
|
1086
1326
|
if (!request) {
|
|
@@ -1121,7 +1361,8 @@ export class SequencerPublisher {
|
|
|
1121
1361
|
}
|
|
1122
1362
|
});
|
|
1123
1363
|
}
|
|
1124
|
-
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber
|
|
1364
|
+
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
|
|
1365
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1125
1366
|
const logData = {
|
|
1126
1367
|
slotNumber,
|
|
1127
1368
|
timestamp,
|
|
@@ -1134,6 +1375,7 @@ export class SequencerPublisher {
|
|
|
1134
1375
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1135
1376
|
this.lastActions[action] = slotNumber;
|
|
1136
1377
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1378
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1137
1379
|
let gasUsed;
|
|
1138
1380
|
const simulateAbi = mergeAbis([
|
|
1139
1381
|
request.abi ?? [],
|
|
@@ -1142,7 +1384,7 @@ export class SequencerPublisher {
|
|
|
1142
1384
|
try {
|
|
1143
1385
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1144
1386
|
time: timestamp
|
|
1145
|
-
}, [], simulateAbi));
|
|
1387
|
+
}, [], simulateAbi));
|
|
1146
1388
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1147
1389
|
...logData,
|
|
1148
1390
|
request,
|
|
@@ -1151,6 +1393,27 @@ export class SequencerPublisher {
|
|
|
1151
1393
|
} catch (err) {
|
|
1152
1394
|
const viemError = formatViemError(err, simulateAbi);
|
|
1153
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
|
+
});
|
|
1154
1417
|
return false;
|
|
1155
1418
|
}
|
|
1156
1419
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
@@ -1196,13 +1459,14 @@ export class SequencerPublisher {
|
|
|
1196
1459
|
* A call to `restart` is required before you can continue publishing.
|
|
1197
1460
|
*/ interrupt() {
|
|
1198
1461
|
this.interrupted = true;
|
|
1462
|
+
this.interruptibleSleep.interrupt();
|
|
1199
1463
|
this.l1TxUtils.interrupt();
|
|
1200
1464
|
}
|
|
1201
1465
|
/** Restarts the publisher after calling `interrupt`. */ restart() {
|
|
1202
1466
|
this.interrupted = false;
|
|
1203
1467
|
this.l1TxUtils.restart();
|
|
1204
1468
|
}
|
|
1205
|
-
async prepareProposeTx(encodedData,
|
|
1469
|
+
async prepareProposeTx(encodedData, options) {
|
|
1206
1470
|
const kzg = Blob.getViemKzgInstance();
|
|
1207
1471
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1208
1472
|
this.log.debug('Validating blob input', {
|
|
@@ -1229,10 +1493,38 @@ export class SequencerPublisher {
|
|
|
1229
1493
|
}, {}, {
|
|
1230
1494
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1231
1495
|
kzg
|
|
1232
|
-
}).catch((err)=>{
|
|
1233
|
-
const
|
|
1234
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1235
|
-
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
|
+
}
|
|
1236
1528
|
});
|
|
1237
1529
|
throw new Error('Failed to validate blobs');
|
|
1238
1530
|
});
|
|
@@ -1243,8 +1535,7 @@ export class SequencerPublisher {
|
|
|
1243
1535
|
header: encodedData.header.toViem(),
|
|
1244
1536
|
archive: toHex(encodedData.archive),
|
|
1245
1537
|
oracleInput: {
|
|
1246
|
-
|
|
1247
|
-
feeAssetPriceModifier: 0n
|
|
1538
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier
|
|
1248
1539
|
}
|
|
1249
1540
|
},
|
|
1250
1541
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1252,7 +1543,7 @@ export class SequencerPublisher {
|
|
|
1252
1543
|
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
1253
1544
|
blobInput
|
|
1254
1545
|
];
|
|
1255
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1546
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
|
|
1256
1547
|
return {
|
|
1257
1548
|
args,
|
|
1258
1549
|
blobEvaluationGas,
|
|
@@ -1263,16 +1554,17 @@ export class SequencerPublisher {
|
|
|
1263
1554
|
/**
|
|
1264
1555
|
* Simulates the propose tx with eth_simulateV1
|
|
1265
1556
|
* @param args - The propose tx args
|
|
1266
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1267
1557
|
* @returns The simulation result
|
|
1268
|
-
*/ async simulateProposeTx(args,
|
|
1558
|
+
*/ async simulateProposeTx(args, options) {
|
|
1269
1559
|
const rollupData = encodeFunctionData({
|
|
1270
1560
|
abi: RollupAbi,
|
|
1271
1561
|
functionName: 'propose',
|
|
1272
1562
|
args
|
|
1273
1563
|
});
|
|
1274
|
-
// override the
|
|
1564
|
+
// override the proposed checkpoint number if requested
|
|
1275
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 ?? []);
|
|
1276
1568
|
const stateOverrides = [
|
|
1277
1569
|
{
|
|
1278
1570
|
address: this.rollupContract.address,
|
|
@@ -1282,7 +1574,8 @@ export class SequencerPublisher {
|
|
|
1282
1574
|
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
1283
1575
|
value: toPaddedHex(0n, true)
|
|
1284
1576
|
},
|
|
1285
|
-
...forcePendingCheckpointNumberStateDiff
|
|
1577
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1578
|
+
...forceProposedFeeHeaderStateDiff
|
|
1286
1579
|
]
|
|
1287
1580
|
}
|
|
1288
1581
|
];
|
|
@@ -1293,6 +1586,8 @@ export class SequencerPublisher {
|
|
|
1293
1586
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1294
1587
|
});
|
|
1295
1588
|
}
|
|
1589
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1590
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1296
1591
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1297
1592
|
to: this.rollupContract.address,
|
|
1298
1593
|
data: rollupData,
|
|
@@ -1301,8 +1596,7 @@ export class SequencerPublisher {
|
|
|
1301
1596
|
from: this.proposerAddressForSimulation.toString()
|
|
1302
1597
|
}
|
|
1303
1598
|
}, {
|
|
1304
|
-
|
|
1305
|
-
time: timestamp + 1n,
|
|
1599
|
+
time: simTs,
|
|
1306
1600
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1307
1601
|
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1308
1602
|
}, stateOverrides, RollupAbi, {
|
|
@@ -1319,7 +1613,29 @@ export class SequencerPublisher {
|
|
|
1319
1613
|
logs: []
|
|
1320
1614
|
};
|
|
1321
1615
|
}
|
|
1322
|
-
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
|
+
});
|
|
1323
1639
|
throw err;
|
|
1324
1640
|
});
|
|
1325
1641
|
return {
|
|
@@ -1327,11 +1643,11 @@ export class SequencerPublisher {
|
|
|
1327
1643
|
simulationResult
|
|
1328
1644
|
};
|
|
1329
1645
|
}
|
|
1330
|
-
async addProposeTx(checkpoint, encodedData, opts = {}
|
|
1646
|
+
async addProposeTx(checkpoint, encodedData, opts = {}) {
|
|
1331
1647
|
const slot = checkpoint.header.slotNumber;
|
|
1332
1648
|
const timer = new Timer();
|
|
1333
1649
|
const kzg = Blob.getViemKzgInstance();
|
|
1334
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData,
|
|
1650
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
|
|
1335
1651
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1336
1652
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
1337
1653
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
@@ -1396,4 +1712,13 @@ export class SequencerPublisher {
|
|
|
1396
1712
|
}
|
|
1397
1713
|
});
|
|
1398
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
|
+
}
|
|
1399
1724
|
}
|