@aztec/sequencer-client 0.0.1-commit.993d52e → 0.0.1-commit.9badcec54
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 +6 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +49 -27
- package/dest/config.d.ts +25 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +27 -21
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +128 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +15 -14
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +16 -50
- package/dest/global_variable_builder/index.d.ts +4 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/publisher/config.d.ts +17 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +25 -5
- 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 +58 -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 +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +16 -3
- package/dest/publisher/sequencer-publisher.d.ts +61 -43
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +392 -119
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +26 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +337 -209
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- 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 +9 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +23 -20
- package/dest/sequencer/sequencer.d.ts +27 -11
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +137 -74
- package/dest/sequencer/timetable.d.ts +17 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -43
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +7 -9
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +39 -30
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +61 -28
- package/src/config.ts +35 -24
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +26 -62
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +47 -4
- 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 +57 -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 +18 -6
- package/src/publisher/sequencer-publisher.ts +437 -171
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +422 -237
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +29 -25
- package/src/sequencer/sequencer.ts +198 -81
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +51 -48
|
@@ -372,34 +372,35 @@ 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 { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3,
|
|
375
|
+
import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, SimulationOverridesBuilder, buildSimulationOverridesStateOverride } 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
|
-
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
381
380
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
381
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
382
382
|
import { pick } from '@aztec/foundation/collection';
|
|
383
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
383
384
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
384
385
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
385
386
|
import { createLogger } from '@aztec/foundation/log';
|
|
386
387
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
388
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
387
389
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
388
390
|
import { Timer } from '@aztec/foundation/timer';
|
|
389
391
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
390
392
|
import { encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
391
393
|
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
394
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
392
395
|
import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
393
|
-
import { encodeFunctionData, toHex } from 'viem';
|
|
396
|
+
import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
|
|
397
|
+
import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
394
398
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
395
399
|
export const Actions = [
|
|
396
400
|
'invalidate-by-invalid-attestation',
|
|
397
401
|
'invalidate-by-insufficient-attestations',
|
|
398
402
|
'propose',
|
|
399
403
|
'governance-signal',
|
|
400
|
-
'empire-slashing-signal',
|
|
401
|
-
'create-empire-payload',
|
|
402
|
-
'execute-empire-payload',
|
|
403
404
|
'vote-offenses',
|
|
404
405
|
'execute-slash'
|
|
405
406
|
];
|
|
@@ -430,6 +431,7 @@ export class SequencerPublisher {
|
|
|
430
431
|
interrupted;
|
|
431
432
|
metrics;
|
|
432
433
|
epochCache;
|
|
434
|
+
failedTxStore;
|
|
433
435
|
governanceLog;
|
|
434
436
|
slashingLog;
|
|
435
437
|
lastActions;
|
|
@@ -437,10 +439,14 @@ export class SequencerPublisher {
|
|
|
437
439
|
payloadProposedCache;
|
|
438
440
|
log;
|
|
439
441
|
ethereumSlotDuration;
|
|
442
|
+
aztecSlotDuration;
|
|
443
|
+
/** Date provider for wall-clock time. */ dateProvider;
|
|
440
444
|
blobClient;
|
|
441
445
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
446
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
|
|
442
447
|
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
443
448
|
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
|
|
449
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */ interruptibleSleep;
|
|
444
450
|
// A CALL to a cold address is 2700 gas
|
|
445
451
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
446
452
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -449,7 +455,6 @@ export class SequencerPublisher {
|
|
|
449
455
|
rollupContract;
|
|
450
456
|
govProposerContract;
|
|
451
457
|
slashingProposerContract;
|
|
452
|
-
slashFactoryContract;
|
|
453
458
|
tracer;
|
|
454
459
|
requests;
|
|
455
460
|
constructor(config, deps){
|
|
@@ -460,16 +465,21 @@ export class SequencerPublisher {
|
|
|
460
465
|
this.lastActions = {};
|
|
461
466
|
this.isPayloadEmptyCache = new Map();
|
|
462
467
|
this.payloadProposedCache = new Set();
|
|
468
|
+
this.interruptibleSleep = new InterruptibleSleep();
|
|
463
469
|
this.requests = [];
|
|
464
470
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
465
471
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
472
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
473
|
+
this.dateProvider = deps.dateProvider;
|
|
466
474
|
this.epochCache = deps.epochCache;
|
|
467
475
|
this.lastActions = deps.lastActions;
|
|
468
476
|
this.blobClient = deps.blobClient;
|
|
477
|
+
this.dateProvider = deps.dateProvider;
|
|
469
478
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
470
479
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
471
480
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
472
481
|
this.l1TxUtils = deps.l1TxUtils;
|
|
482
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
473
483
|
this.rollupContract = deps.rollupContract;
|
|
474
484
|
this.govProposerContract = deps.governanceProposerContract;
|
|
475
485
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -478,13 +488,30 @@ export class SequencerPublisher {
|
|
|
478
488
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
479
489
|
this.slashingProposerContract = newSlashingProposer;
|
|
480
490
|
});
|
|
481
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
482
491
|
// Initialize L1 fee analyzer for fisherman mode
|
|
483
492
|
if (config.fishermanMode) {
|
|
484
493
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
485
494
|
}
|
|
486
495
|
// Initialize fee asset price oracle
|
|
487
496
|
this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
|
|
497
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
498
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
502
|
+
* Does nothing if no store is configured.
|
|
503
|
+
*/ backupFailedTx(failedTx) {
|
|
504
|
+
if (!this.failedTxStore) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const tx = {
|
|
508
|
+
...failedTx,
|
|
509
|
+
timestamp: Date.now()
|
|
510
|
+
};
|
|
511
|
+
// Fire and forget - don't block on backup
|
|
512
|
+
void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
|
|
513
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
514
|
+
});
|
|
488
515
|
}
|
|
489
516
|
getRollupContract() {
|
|
490
517
|
return this.rollupContract;
|
|
@@ -513,7 +540,7 @@ export class SequencerPublisher {
|
|
|
513
540
|
this.requests.push(request);
|
|
514
541
|
}
|
|
515
542
|
getCurrentL2Slot() {
|
|
516
|
-
return this.epochCache.
|
|
543
|
+
return this.epochCache.getSlotNow();
|
|
517
544
|
}
|
|
518
545
|
/**
|
|
519
546
|
* Clears all pending requests without sending them.
|
|
@@ -602,8 +629,8 @@ export class SequencerPublisher {
|
|
|
602
629
|
// @note - we can only have one blob config per bundle
|
|
603
630
|
// find requests with gas and blob configs
|
|
604
631
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
605
|
-
const gasConfigs =
|
|
606
|
-
const blobConfigs =
|
|
632
|
+
const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
|
|
633
|
+
const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
|
|
607
634
|
if (blobConfigs.length > 1) {
|
|
608
635
|
throw new Error('Multiple blob configs found');
|
|
609
636
|
}
|
|
@@ -630,12 +657,34 @@ export class SequencerPublisher {
|
|
|
630
657
|
// This ensures the committee gets precomputed correctly
|
|
631
658
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
632
659
|
try {
|
|
660
|
+
// Capture context for failed tx backup before sending
|
|
661
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
662
|
+
const multicallData = encodeFunctionData({
|
|
663
|
+
abi: multicall3Abi,
|
|
664
|
+
functionName: 'aggregate3',
|
|
665
|
+
args: [
|
|
666
|
+
validRequests.map((r)=>({
|
|
667
|
+
target: r.request.to,
|
|
668
|
+
callData: r.request.data,
|
|
669
|
+
allowFailure: true
|
|
670
|
+
}))
|
|
671
|
+
]
|
|
672
|
+
});
|
|
673
|
+
const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
|
|
674
|
+
const txContext = {
|
|
675
|
+
multicallData,
|
|
676
|
+
blobData: blobDataHex,
|
|
677
|
+
l1BlockNumber
|
|
678
|
+
};
|
|
633
679
|
this.log.debug('Forwarding transactions', {
|
|
634
680
|
validRequests: validRequests.map((request)=>request.action),
|
|
635
681
|
txConfig
|
|
636
682
|
});
|
|
637
|
-
const result = await
|
|
638
|
-
|
|
683
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
684
|
+
if (result === undefined) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
|
|
639
688
|
return {
|
|
640
689
|
result,
|
|
641
690
|
expiredActions,
|
|
@@ -655,17 +704,118 @@ export class SequencerPublisher {
|
|
|
655
704
|
}
|
|
656
705
|
}
|
|
657
706
|
}
|
|
658
|
-
|
|
707
|
+
/**
|
|
708
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
709
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
710
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
711
|
+
*/ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
|
|
712
|
+
const triedAddresses = [];
|
|
713
|
+
let currentPublisher = this.l1TxUtils;
|
|
714
|
+
while(true){
|
|
715
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
716
|
+
try {
|
|
717
|
+
const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
|
|
718
|
+
this.l1TxUtils = currentPublisher;
|
|
719
|
+
return result;
|
|
720
|
+
} catch (err) {
|
|
721
|
+
if (err instanceof TimeoutError) {
|
|
722
|
+
throw err;
|
|
723
|
+
}
|
|
724
|
+
const viemError = formatViemError(err);
|
|
725
|
+
if (!this.getNextPublisher) {
|
|
726
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
|
|
730
|
+
const nextPublisher = await this.getNextPublisher([
|
|
731
|
+
...triedAddresses
|
|
732
|
+
]);
|
|
733
|
+
if (!nextPublisher) {
|
|
734
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
currentPublisher = nextPublisher;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/*
|
|
742
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
743
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
744
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
745
|
+
*/ async sendRequestsAt(submitAfter) {
|
|
746
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
747
|
+
if (ms > 0) {
|
|
748
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, {
|
|
749
|
+
submitAfter
|
|
750
|
+
});
|
|
751
|
+
await this.interruptibleSleep.sleep(ms);
|
|
752
|
+
}
|
|
753
|
+
if (this.interrupted) {
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
// Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
|
|
757
|
+
const validRequests = [];
|
|
758
|
+
for (const request of this.requests){
|
|
759
|
+
if (!request.preCheck) {
|
|
760
|
+
validRequests.push(request);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
await request.preCheck();
|
|
765
|
+
validRequests.push(request);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
this.requests = validRequests;
|
|
771
|
+
if (this.requests.length === 0) {
|
|
772
|
+
return undefined;
|
|
773
|
+
}
|
|
774
|
+
return this.sendRequests();
|
|
775
|
+
}
|
|
776
|
+
callbackBundledTransactions(requests, result, txContext) {
|
|
659
777
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
660
778
|
if (result instanceof FormattedViemError) {
|
|
661
779
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
780
|
+
this.backupFailedTx({
|
|
781
|
+
id: keccak256(txContext.multicallData),
|
|
782
|
+
failureType: 'send-error',
|
|
783
|
+
request: {
|
|
784
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
785
|
+
data: txContext.multicallData
|
|
786
|
+
},
|
|
787
|
+
blobData: txContext.blobData,
|
|
788
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
789
|
+
error: {
|
|
790
|
+
message: result.message,
|
|
791
|
+
name: result.name
|
|
792
|
+
},
|
|
793
|
+
context: {
|
|
794
|
+
actions: requests.map((r)=>r.action),
|
|
795
|
+
requests: requests.map((r)=>({
|
|
796
|
+
action: r.action,
|
|
797
|
+
to: r.request.to,
|
|
798
|
+
data: r.request.data
|
|
799
|
+
})),
|
|
800
|
+
sender: this.getSenderAddress().toString()
|
|
801
|
+
}
|
|
802
|
+
});
|
|
662
803
|
return {
|
|
663
804
|
failedActions: requests.map((r)=>r.action)
|
|
664
805
|
};
|
|
665
806
|
} else {
|
|
666
807
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
667
808
|
result,
|
|
668
|
-
requests
|
|
809
|
+
requests: requests.map((r)=>({
|
|
810
|
+
...r,
|
|
811
|
+
// Avoid logging large blob data
|
|
812
|
+
blobConfig: r.blobConfig ? {
|
|
813
|
+
...r.blobConfig,
|
|
814
|
+
blobs: r.blobConfig.blobs.map((b)=>({
|
|
815
|
+
size: trimmedBytesLength(b)
|
|
816
|
+
}))
|
|
817
|
+
} : undefined
|
|
818
|
+
}))
|
|
669
819
|
});
|
|
670
820
|
const successfulActions = [];
|
|
671
821
|
const failedActions = [];
|
|
@@ -676,6 +826,37 @@ export class SequencerPublisher {
|
|
|
676
826
|
failedActions.push(request.action);
|
|
677
827
|
}
|
|
678
828
|
}
|
|
829
|
+
// Single backup for the whole reverted tx
|
|
830
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
831
|
+
this.backupFailedTx({
|
|
832
|
+
id: result.receipt.transactionHash,
|
|
833
|
+
failureType: 'revert',
|
|
834
|
+
request: {
|
|
835
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
836
|
+
data: txContext.multicallData
|
|
837
|
+
},
|
|
838
|
+
blobData: txContext.blobData,
|
|
839
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
840
|
+
receipt: {
|
|
841
|
+
transactionHash: result.receipt.transactionHash,
|
|
842
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
843
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
844
|
+
status: 'reverted'
|
|
845
|
+
},
|
|
846
|
+
error: {
|
|
847
|
+
message: result.errorMsg ?? 'Transaction reverted'
|
|
848
|
+
},
|
|
849
|
+
context: {
|
|
850
|
+
actions: failedActions,
|
|
851
|
+
requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
|
|
852
|
+
action: r.action,
|
|
853
|
+
to: r.request.to,
|
|
854
|
+
data: r.request.data
|
|
855
|
+
})),
|
|
856
|
+
sender: this.getSenderAddress().toString()
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
679
860
|
return {
|
|
680
861
|
successfulActions,
|
|
681
862
|
failedActions
|
|
@@ -683,19 +864,20 @@ export class SequencerPublisher {
|
|
|
683
864
|
}
|
|
684
865
|
}
|
|
685
866
|
/**
|
|
686
|
-
* @notice Will call `
|
|
867
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
687
868
|
* @param tipArchive - The archive to check
|
|
688
869
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
689
|
-
*/
|
|
870
|
+
*/ async canProposeAt(tipArchive, msgSender, simulationOverridesPlan) {
|
|
690
871
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
691
872
|
const ignoredErrors = [
|
|
692
873
|
'SlotAlreadyInChain',
|
|
693
874
|
'InvalidProposer',
|
|
694
875
|
'InvalidArchive'
|
|
695
876
|
];
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
877
|
+
const pipelined = this.epochCache.isProposerPipeliningEnabled();
|
|
878
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
879
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
880
|
+
return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan)).catch((err)=>{
|
|
699
881
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
700
882
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
701
883
|
error: err.message
|
|
@@ -711,7 +893,7 @@ export class SequencerPublisher {
|
|
|
711
893
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
712
894
|
* It will throw if the block header is invalid.
|
|
713
895
|
* @param header - The block header to validate
|
|
714
|
-
*/ async validateBlockHeader(header,
|
|
896
|
+
*/ async validateBlockHeader(header, simulationOverridesPlan) {
|
|
715
897
|
const flags = {
|
|
716
898
|
ignoreDA: true,
|
|
717
899
|
ignoreSignatures: true
|
|
@@ -725,8 +907,8 @@ export class SequencerPublisher {
|
|
|
725
907
|
header.blobsHash.toString(),
|
|
726
908
|
flags
|
|
727
909
|
];
|
|
728
|
-
const ts =
|
|
729
|
-
const stateOverrides = await this.rollupContract
|
|
910
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
911
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
730
912
|
let balance = 0n;
|
|
731
913
|
if (this.config.fishermanMode) {
|
|
732
914
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -748,7 +930,7 @@ export class SequencerPublisher {
|
|
|
748
930
|
}),
|
|
749
931
|
from: MULTI_CALL_3_ADDRESS
|
|
750
932
|
}, {
|
|
751
|
-
time: ts
|
|
933
|
+
time: ts
|
|
752
934
|
}, stateOverrides);
|
|
753
935
|
this.log.debug(`Simulated validateHeader`);
|
|
754
936
|
}
|
|
@@ -778,6 +960,7 @@ export class SequencerPublisher {
|
|
|
778
960
|
...logData,
|
|
779
961
|
request
|
|
780
962
|
});
|
|
963
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
781
964
|
try {
|
|
782
965
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
783
966
|
request.abi ?? [],
|
|
@@ -793,6 +976,7 @@ export class SequencerPublisher {
|
|
|
793
976
|
gasUsed,
|
|
794
977
|
checkpointNumber,
|
|
795
978
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
979
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
796
980
|
reason
|
|
797
981
|
};
|
|
798
982
|
} catch (err) {
|
|
@@ -805,8 +989,8 @@ export class SequencerPublisher {
|
|
|
805
989
|
request,
|
|
806
990
|
error: viemError.message
|
|
807
991
|
});
|
|
808
|
-
const
|
|
809
|
-
if (
|
|
992
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
993
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
810
994
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
|
|
811
995
|
...logData
|
|
812
996
|
});
|
|
@@ -820,6 +1004,27 @@ export class SequencerPublisher {
|
|
|
820
1004
|
}
|
|
821
1005
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
822
1006
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
1007
|
+
this.backupFailedTx({
|
|
1008
|
+
id: keccak256(request.data),
|
|
1009
|
+
failureType: 'simulation',
|
|
1010
|
+
request: {
|
|
1011
|
+
to: request.to,
|
|
1012
|
+
data: request.data,
|
|
1013
|
+
value: request.value?.toString()
|
|
1014
|
+
},
|
|
1015
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1016
|
+
error: {
|
|
1017
|
+
message: viemError.message,
|
|
1018
|
+
name: viemError.name
|
|
1019
|
+
},
|
|
1020
|
+
context: {
|
|
1021
|
+
actions: [
|
|
1022
|
+
`invalidate-${reason}`
|
|
1023
|
+
],
|
|
1024
|
+
checkpointNumber,
|
|
1025
|
+
sender: this.getSenderAddress().toString()
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
823
1028
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
824
1029
|
cause: viemError
|
|
825
1030
|
});
|
|
@@ -845,8 +1050,7 @@ export class SequencerPublisher {
|
|
|
845
1050
|
throw new Error(`Unknown reason for invalidation`);
|
|
846
1051
|
}
|
|
847
1052
|
}
|
|
848
|
-
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature,
|
|
849
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
1053
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan) {
|
|
850
1054
|
const blobFields = checkpoint.toBlobFields();
|
|
851
1055
|
const blobs = await getBlobsPerL1Block(blobFields);
|
|
852
1056
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
@@ -863,10 +1067,9 @@ export class SequencerPublisher {
|
|
|
863
1067
|
attestationsAndSignersSignature.toViemSignature(),
|
|
864
1068
|
blobInput
|
|
865
1069
|
];
|
|
866
|
-
await this.simulateProposeTx(args,
|
|
867
|
-
return ts;
|
|
1070
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
868
1071
|
}
|
|
869
|
-
async enqueueCastSignalHelper(slotNumber,
|
|
1072
|
+
async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
|
|
870
1073
|
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
871
1074
|
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
872
1075
|
return false;
|
|
@@ -922,6 +1125,8 @@ export class SequencerPublisher {
|
|
|
922
1125
|
signer: this.l1TxUtils.client.account?.address,
|
|
923
1126
|
lastValidL2Slot: slotNumber
|
|
924
1127
|
});
|
|
1128
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1129
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
925
1130
|
try {
|
|
926
1131
|
await this.l1TxUtils.simulate(request, {
|
|
927
1132
|
time: timestamp
|
|
@@ -933,7 +1138,32 @@ export class SequencerPublisher {
|
|
|
933
1138
|
request
|
|
934
1139
|
});
|
|
935
1140
|
} catch (err) {
|
|
936
|
-
|
|
1141
|
+
const viemError = formatViemError(err);
|
|
1142
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
1143
|
+
simulationTimestamp: timestamp,
|
|
1144
|
+
l1BlockNumber
|
|
1145
|
+
});
|
|
1146
|
+
this.backupFailedTx({
|
|
1147
|
+
id: keccak256(request.data),
|
|
1148
|
+
failureType: 'simulation',
|
|
1149
|
+
request: {
|
|
1150
|
+
to: request.to,
|
|
1151
|
+
data: request.data,
|
|
1152
|
+
value: request.value?.toString()
|
|
1153
|
+
},
|
|
1154
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1155
|
+
error: {
|
|
1156
|
+
message: viemError.message,
|
|
1157
|
+
name: viemError.name
|
|
1158
|
+
},
|
|
1159
|
+
context: {
|
|
1160
|
+
actions: [
|
|
1161
|
+
action
|
|
1162
|
+
],
|
|
1163
|
+
slot: slotNumber,
|
|
1164
|
+
sender: this.getSenderAddress().toString()
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
937
1167
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
938
1168
|
}
|
|
939
1169
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -977,55 +1207,17 @@ export class SequencerPublisher {
|
|
|
977
1207
|
/**
|
|
978
1208
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
979
1209
|
* @param slotNumber - The slot number to cast a signal for.
|
|
980
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
981
1210
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
982
|
-
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber,
|
|
983
|
-
return this.enqueueCastSignalHelper(slotNumber,
|
|
1211
|
+
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
|
|
1212
|
+
return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
984
1213
|
}
|
|
985
|
-
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber,
|
|
1214
|
+
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
|
|
986
1215
|
if (actions.length === 0) {
|
|
987
1216
|
this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
|
|
988
1217
|
return false;
|
|
989
1218
|
}
|
|
990
1219
|
for (const action of actions){
|
|
991
1220
|
switch(action.type){
|
|
992
|
-
case 'vote-empire-payload':
|
|
993
|
-
{
|
|
994
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
995
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
996
|
-
break;
|
|
997
|
-
}
|
|
998
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
999
|
-
signerAddress
|
|
1000
|
-
});
|
|
1001
|
-
await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
case 'create-empire-payload':
|
|
1005
|
-
{
|
|
1006
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, {
|
|
1007
|
-
slotNumber,
|
|
1008
|
-
signerAddress
|
|
1009
|
-
});
|
|
1010
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
1011
|
-
await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, timestamp);
|
|
1012
|
-
break;
|
|
1013
|
-
}
|
|
1014
|
-
case 'execute-empire-payload':
|
|
1015
|
-
{
|
|
1016
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, {
|
|
1017
|
-
slotNumber,
|
|
1018
|
-
signerAddress
|
|
1019
|
-
});
|
|
1020
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1021
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
1022
|
-
return false;
|
|
1023
|
-
}
|
|
1024
|
-
const empireSlashingProposer = this.slashingProposerContract;
|
|
1025
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1026
|
-
await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, timestamp);
|
|
1027
|
-
break;
|
|
1028
|
-
}
|
|
1029
1221
|
case 'vote-offenses':
|
|
1030
1222
|
{
|
|
1031
1223
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
@@ -1034,14 +1226,13 @@ export class SequencerPublisher {
|
|
|
1034
1226
|
votesCount: action.votes.length,
|
|
1035
1227
|
signerAddress
|
|
1036
1228
|
});
|
|
1037
|
-
if (this.slashingProposerContract
|
|
1038
|
-
this.log.error('
|
|
1229
|
+
if (!this.slashingProposerContract) {
|
|
1230
|
+
this.log.error('No slashing proposer contract available');
|
|
1039
1231
|
return false;
|
|
1040
1232
|
}
|
|
1041
|
-
const tallySlashingProposer = this.slashingProposerContract;
|
|
1042
1233
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1043
|
-
const request = await
|
|
1044
|
-
await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!
|
|
1234
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1235
|
+
await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!this.slashingProposerContract.tryExtractVoteCastEvent(receipt.logs), slotNumber);
|
|
1045
1236
|
break;
|
|
1046
1237
|
}
|
|
1047
1238
|
case 'execute-slash':
|
|
@@ -1051,13 +1242,12 @@ export class SequencerPublisher {
|
|
|
1051
1242
|
round: action.round,
|
|
1052
1243
|
signerAddress
|
|
1053
1244
|
});
|
|
1054
|
-
if (this.slashingProposerContract
|
|
1055
|
-
this.log.error('
|
|
1245
|
+
if (!this.slashingProposerContract) {
|
|
1246
|
+
this.log.error('No slashing proposer contract available');
|
|
1056
1247
|
return false;
|
|
1057
1248
|
}
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber, timestamp);
|
|
1249
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(action.round, action.committees);
|
|
1250
|
+
await this.simulateAndEnqueueRequest('execute-slash', executeRequest, (receipt)=>!!this.slashingProposerContract.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
|
|
1061
1251
|
break;
|
|
1062
1252
|
}
|
|
1063
1253
|
default:
|
|
@@ -1081,27 +1271,40 @@ export class SequencerPublisher {
|
|
|
1081
1271
|
attestationsAndSignersSignature,
|
|
1082
1272
|
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1083
1273
|
};
|
|
1084
|
-
|
|
1274
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan).withoutBlobCheck().build();
|
|
1085
1275
|
try {
|
|
1086
1276
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
1087
1277
|
// This means that we can avoid the simulation issues in later checks.
|
|
1088
1278
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1089
1279
|
// make time consistency checks break.
|
|
1090
1280
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1091
|
-
|
|
1281
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
|
|
1092
1282
|
} catch (err) {
|
|
1093
1283
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1094
1284
|
...checkpoint.getStats(),
|
|
1095
1285
|
slotNumber: checkpoint.header.slotNumber,
|
|
1096
|
-
|
|
1286
|
+
simulationOverridesPlan
|
|
1097
1287
|
});
|
|
1098
1288
|
throw err;
|
|
1099
1289
|
}
|
|
1290
|
+
// Build a pre-check callback that re-validates the checkpoint before L1 submission.
|
|
1291
|
+
// During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
|
|
1292
|
+
let preCheck = undefined;
|
|
1293
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
1294
|
+
preCheck = async ()=>{
|
|
1295
|
+
this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
|
|
1296
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1100
1299
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1101
1300
|
...checkpoint.toCheckpointInfo(),
|
|
1102
|
-
|
|
1301
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1302
|
+
simulationOverridesPlan
|
|
1103
1303
|
});
|
|
1104
|
-
await this.addProposeTx(checkpoint, proposeTxArgs,
|
|
1304
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, {
|
|
1305
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1306
|
+
simulationOverridesPlan
|
|
1307
|
+
}, preCheck);
|
|
1105
1308
|
}
|
|
1106
1309
|
enqueueInvalidateCheckpoint(request, opts = {}) {
|
|
1107
1310
|
if (!request) {
|
|
@@ -1142,7 +1345,8 @@ export class SequencerPublisher {
|
|
|
1142
1345
|
}
|
|
1143
1346
|
});
|
|
1144
1347
|
}
|
|
1145
|
-
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber
|
|
1348
|
+
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
|
|
1349
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1146
1350
|
const logData = {
|
|
1147
1351
|
slotNumber,
|
|
1148
1352
|
timestamp,
|
|
@@ -1155,6 +1359,7 @@ export class SequencerPublisher {
|
|
|
1155
1359
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1156
1360
|
this.lastActions[action] = slotNumber;
|
|
1157
1361
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1362
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1158
1363
|
let gasUsed;
|
|
1159
1364
|
const simulateAbi = mergeAbis([
|
|
1160
1365
|
request.abi ?? [],
|
|
@@ -1163,7 +1368,7 @@ export class SequencerPublisher {
|
|
|
1163
1368
|
try {
|
|
1164
1369
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1165
1370
|
time: timestamp
|
|
1166
|
-
}, [], simulateAbi));
|
|
1371
|
+
}, [], simulateAbi));
|
|
1167
1372
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1168
1373
|
...logData,
|
|
1169
1374
|
request,
|
|
@@ -1172,6 +1377,27 @@ export class SequencerPublisher {
|
|
|
1172
1377
|
} catch (err) {
|
|
1173
1378
|
const viemError = formatViemError(err, simulateAbi);
|
|
1174
1379
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1380
|
+
this.backupFailedTx({
|
|
1381
|
+
id: keccak256(request.data),
|
|
1382
|
+
failureType: 'simulation',
|
|
1383
|
+
request: {
|
|
1384
|
+
to: request.to,
|
|
1385
|
+
data: request.data,
|
|
1386
|
+
value: request.value?.toString()
|
|
1387
|
+
},
|
|
1388
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1389
|
+
error: {
|
|
1390
|
+
message: viemError.message,
|
|
1391
|
+
name: viemError.name
|
|
1392
|
+
},
|
|
1393
|
+
context: {
|
|
1394
|
+
actions: [
|
|
1395
|
+
action
|
|
1396
|
+
],
|
|
1397
|
+
slot: slotNumber,
|
|
1398
|
+
sender: this.getSenderAddress().toString()
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1175
1401
|
return false;
|
|
1176
1402
|
}
|
|
1177
1403
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
@@ -1217,13 +1443,14 @@ export class SequencerPublisher {
|
|
|
1217
1443
|
* A call to `restart` is required before you can continue publishing.
|
|
1218
1444
|
*/ interrupt() {
|
|
1219
1445
|
this.interrupted = true;
|
|
1446
|
+
this.interruptibleSleep.interrupt();
|
|
1220
1447
|
this.l1TxUtils.interrupt();
|
|
1221
1448
|
}
|
|
1222
1449
|
/** Restarts the publisher after calling `interrupt`. */ restart() {
|
|
1223
1450
|
this.interrupted = false;
|
|
1224
1451
|
this.l1TxUtils.restart();
|
|
1225
1452
|
}
|
|
1226
|
-
async prepareProposeTx(encodedData,
|
|
1453
|
+
async prepareProposeTx(encodedData, simulationOverridesPlan) {
|
|
1227
1454
|
const kzg = Blob.getViemKzgInstance();
|
|
1228
1455
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1229
1456
|
this.log.debug('Validating blob input', {
|
|
@@ -1250,10 +1477,38 @@ export class SequencerPublisher {
|
|
|
1250
1477
|
}, {}, {
|
|
1251
1478
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1252
1479
|
kzg
|
|
1253
|
-
}).catch((err)=>{
|
|
1254
|
-
const
|
|
1255
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1256
|
-
metaMessages
|
|
1480
|
+
}).catch(async (err)=>{
|
|
1481
|
+
const viemError = formatViemError(err);
|
|
1482
|
+
this.log.error(`Failed to validate blobs`, viemError.message, {
|
|
1483
|
+
metaMessages: viemError.metaMessages
|
|
1484
|
+
});
|
|
1485
|
+
const validateBlobsData = encodeFunctionData({
|
|
1486
|
+
abi: RollupAbi,
|
|
1487
|
+
functionName: 'validateBlobs',
|
|
1488
|
+
args: [
|
|
1489
|
+
blobInput
|
|
1490
|
+
]
|
|
1491
|
+
});
|
|
1492
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1493
|
+
this.backupFailedTx({
|
|
1494
|
+
id: keccak256(validateBlobsData),
|
|
1495
|
+
failureType: 'simulation',
|
|
1496
|
+
request: {
|
|
1497
|
+
to: this.rollupContract.address,
|
|
1498
|
+
data: validateBlobsData
|
|
1499
|
+
},
|
|
1500
|
+
blobData: encodedData.blobs.map((b)=>toHex(b.data)),
|
|
1501
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1502
|
+
error: {
|
|
1503
|
+
message: viemError.message,
|
|
1504
|
+
name: viemError.name
|
|
1505
|
+
},
|
|
1506
|
+
context: {
|
|
1507
|
+
actions: [
|
|
1508
|
+
'validate-blobs'
|
|
1509
|
+
],
|
|
1510
|
+
sender: this.getSenderAddress().toString()
|
|
1511
|
+
}
|
|
1257
1512
|
});
|
|
1258
1513
|
throw new Error('Failed to validate blobs');
|
|
1259
1514
|
});
|
|
@@ -1272,7 +1527,7 @@ export class SequencerPublisher {
|
|
|
1272
1527
|
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
1273
1528
|
blobInput
|
|
1274
1529
|
];
|
|
1275
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1530
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1276
1531
|
return {
|
|
1277
1532
|
args,
|
|
1278
1533
|
blobEvaluationGas,
|
|
@@ -1283,29 +1538,14 @@ export class SequencerPublisher {
|
|
|
1283
1538
|
/**
|
|
1284
1539
|
* Simulates the propose tx with eth_simulateV1
|
|
1285
1540
|
* @param args - The propose tx args
|
|
1286
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1287
1541
|
* @returns The simulation result
|
|
1288
|
-
*/ async simulateProposeTx(args,
|
|
1542
|
+
*/ async simulateProposeTx(args, simulationOverridesPlan) {
|
|
1289
1543
|
const rollupData = encodeFunctionData({
|
|
1290
1544
|
abi: RollupAbi,
|
|
1291
1545
|
functionName: 'propose',
|
|
1292
1546
|
args
|
|
1293
1547
|
});
|
|
1294
|
-
|
|
1295
|
-
const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1296
|
-
const stateOverrides = [
|
|
1297
|
-
{
|
|
1298
|
-
address: this.rollupContract.address,
|
|
1299
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1300
|
-
stateDiff: [
|
|
1301
|
-
{
|
|
1302
|
-
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
1303
|
-
value: toPaddedHex(0n, true)
|
|
1304
|
-
},
|
|
1305
|
-
...forcePendingCheckpointNumberStateDiff
|
|
1306
|
-
]
|
|
1307
|
-
}
|
|
1308
|
-
];
|
|
1548
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1309
1549
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1310
1550
|
if (this.proposerAddressForSimulation) {
|
|
1311
1551
|
stateOverrides.push({
|
|
@@ -1313,6 +1553,8 @@ export class SequencerPublisher {
|
|
|
1313
1553
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1314
1554
|
});
|
|
1315
1555
|
}
|
|
1556
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1557
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1316
1558
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1317
1559
|
to: this.rollupContract.address,
|
|
1318
1560
|
data: rollupData,
|
|
@@ -1321,8 +1563,7 @@ export class SequencerPublisher {
|
|
|
1321
1563
|
from: this.proposerAddressForSimulation.toString()
|
|
1322
1564
|
}
|
|
1323
1565
|
}, {
|
|
1324
|
-
|
|
1325
|
-
time: timestamp + 1n,
|
|
1566
|
+
time: simTs,
|
|
1326
1567
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1327
1568
|
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1328
1569
|
}, stateOverrides, RollupAbi, {
|
|
@@ -1339,7 +1580,29 @@ export class SequencerPublisher {
|
|
|
1339
1580
|
logs: []
|
|
1340
1581
|
};
|
|
1341
1582
|
}
|
|
1342
|
-
this.log.error(`Failed to simulate propose tx`, viemError
|
|
1583
|
+
this.log.error(`Failed to simulate propose tx`, viemError, {
|
|
1584
|
+
simulationTimestamp: simTs
|
|
1585
|
+
});
|
|
1586
|
+
this.backupFailedTx({
|
|
1587
|
+
id: keccak256(rollupData),
|
|
1588
|
+
failureType: 'simulation',
|
|
1589
|
+
request: {
|
|
1590
|
+
to: this.rollupContract.address,
|
|
1591
|
+
data: rollupData
|
|
1592
|
+
},
|
|
1593
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1594
|
+
error: {
|
|
1595
|
+
message: viemError.message,
|
|
1596
|
+
name: viemError.name
|
|
1597
|
+
},
|
|
1598
|
+
context: {
|
|
1599
|
+
actions: [
|
|
1600
|
+
'propose'
|
|
1601
|
+
],
|
|
1602
|
+
slot: Number(args[0].header.slotNumber),
|
|
1603
|
+
sender: this.getSenderAddress().toString()
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1343
1606
|
throw err;
|
|
1344
1607
|
});
|
|
1345
1608
|
return {
|
|
@@ -1347,11 +1610,11 @@ export class SequencerPublisher {
|
|
|
1347
1610
|
simulationResult
|
|
1348
1611
|
};
|
|
1349
1612
|
}
|
|
1350
|
-
async addProposeTx(checkpoint, encodedData, opts = {},
|
|
1613
|
+
async addProposeTx(checkpoint, encodedData, opts = {}, preCheck) {
|
|
1351
1614
|
const slot = checkpoint.header.slotNumber;
|
|
1352
1615
|
const timer = new Timer();
|
|
1353
1616
|
const kzg = Blob.getViemKzgInstance();
|
|
1354
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData,
|
|
1617
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts.simulationOverridesPlan);
|
|
1355
1618
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1356
1619
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
1357
1620
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
@@ -1367,9 +1630,10 @@ export class SequencerPublisher {
|
|
|
1367
1630
|
},
|
|
1368
1631
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1369
1632
|
gasConfig: {
|
|
1370
|
-
|
|
1633
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1371
1634
|
gasLimit
|
|
1372
1635
|
},
|
|
1636
|
+
preCheck,
|
|
1373
1637
|
blobConfig: {
|
|
1374
1638
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1375
1639
|
kzg
|
|
@@ -1416,4 +1680,13 @@ export class SequencerPublisher {
|
|
|
1416
1680
|
}
|
|
1417
1681
|
});
|
|
1418
1682
|
}
|
|
1683
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1684
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
|
|
1685
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1686
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1687
|
+
}
|
|
1688
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
|
|
1689
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1690
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1691
|
+
}
|
|
1419
1692
|
}
|