@aztec/sequencer-client 0.0.1-commit.ee80a48 → 0.0.1-commit.f103f88
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 +17 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +63 -30
- package/dest/config.d.ts +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +52 -36
- 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 -16
- 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 +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 +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 +11 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -3
- package/dest/publisher/sequencer-publisher.d.ts +75 -48
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +432 -139
- 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 +28 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +381 -214
- 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 +19 -8
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +95 -21
- package/dest/sequencer/sequencer.d.ts +43 -17
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +158 -96
- package/dest/sequencer/timetable.d.ts +17 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -46
- 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 +81 -30
- package/src/config.ts +70 -46
- 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 +27 -63
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +157 -45
- 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 +38 -9
- package/src/publisher/sequencer-publisher.ts +499 -198
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +499 -234
- 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 +104 -26
- package/src/sequencer/sequencer.ts +227 -108
- package/src/sequencer/timetable.ts +70 -57
- 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,33 +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 { 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';
|
|
387
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
388
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
386
389
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
387
390
|
import { Timer } from '@aztec/foundation/timer';
|
|
388
391
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
389
392
|
import { encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
390
393
|
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
394
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
391
395
|
import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
392
|
-
import { encodeFunctionData, toHex } from 'viem';
|
|
396
|
+
import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
|
|
397
|
+
import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
393
398
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
394
399
|
export const Actions = [
|
|
395
400
|
'invalidate-by-invalid-attestation',
|
|
396
401
|
'invalidate-by-insufficient-attestations',
|
|
397
402
|
'propose',
|
|
398
403
|
'governance-signal',
|
|
399
|
-
'empire-slashing-signal',
|
|
400
|
-
'create-empire-payload',
|
|
401
|
-
'execute-empire-payload',
|
|
402
404
|
'vote-offenses',
|
|
403
405
|
'execute-slash'
|
|
404
406
|
];
|
|
@@ -429,15 +431,22 @@ export class SequencerPublisher {
|
|
|
429
431
|
interrupted;
|
|
430
432
|
metrics;
|
|
431
433
|
epochCache;
|
|
434
|
+
failedTxStore;
|
|
432
435
|
governanceLog;
|
|
433
436
|
slashingLog;
|
|
434
437
|
lastActions;
|
|
435
438
|
isPayloadEmptyCache;
|
|
439
|
+
payloadProposedCache;
|
|
436
440
|
log;
|
|
437
441
|
ethereumSlotDuration;
|
|
442
|
+
aztecSlotDuration;
|
|
443
|
+
/** Date provider for wall-clock time. */ dateProvider;
|
|
438
444
|
blobClient;
|
|
439
445
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
446
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
|
|
440
447
|
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
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;
|
|
441
450
|
// A CALL to a cold address is 2700 gas
|
|
442
451
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
443
452
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -446,7 +455,6 @@ export class SequencerPublisher {
|
|
|
446
455
|
rollupContract;
|
|
447
456
|
govProposerContract;
|
|
448
457
|
slashingProposerContract;
|
|
449
|
-
slashFactoryContract;
|
|
450
458
|
tracer;
|
|
451
459
|
requests;
|
|
452
460
|
constructor(config, deps){
|
|
@@ -456,16 +464,22 @@ export class SequencerPublisher {
|
|
|
456
464
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
457
465
|
this.lastActions = {};
|
|
458
466
|
this.isPayloadEmptyCache = new Map();
|
|
467
|
+
this.payloadProposedCache = new Set();
|
|
468
|
+
this.interruptibleSleep = new InterruptibleSleep();
|
|
459
469
|
this.requests = [];
|
|
460
470
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
461
471
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
472
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
473
|
+
this.dateProvider = deps.dateProvider;
|
|
462
474
|
this.epochCache = deps.epochCache;
|
|
463
475
|
this.lastActions = deps.lastActions;
|
|
464
476
|
this.blobClient = deps.blobClient;
|
|
477
|
+
this.dateProvider = deps.dateProvider;
|
|
465
478
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
466
479
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
467
480
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
468
481
|
this.l1TxUtils = deps.l1TxUtils;
|
|
482
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
469
483
|
this.rollupContract = deps.rollupContract;
|
|
470
484
|
this.govProposerContract = deps.governanceProposerContract;
|
|
471
485
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -474,15 +488,40 @@ export class SequencerPublisher {
|
|
|
474
488
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
475
489
|
this.slashingProposerContract = newSlashingProposer;
|
|
476
490
|
});
|
|
477
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
478
491
|
// Initialize L1 fee analyzer for fisherman mode
|
|
479
492
|
if (config.fishermanMode) {
|
|
480
493
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
481
494
|
}
|
|
495
|
+
// Initialize fee asset price oracle
|
|
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
|
+
});
|
|
482
515
|
}
|
|
483
516
|
getRollupContract() {
|
|
484
517
|
return this.rollupContract;
|
|
485
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Gets the fee asset price modifier from the oracle.
|
|
521
|
+
* Returns 0n if the oracle query fails.
|
|
522
|
+
*/ getFeeAssetPriceModifier() {
|
|
523
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
524
|
+
}
|
|
486
525
|
getSenderAddress() {
|
|
487
526
|
return this.l1TxUtils.getSenderAddress();
|
|
488
527
|
}
|
|
@@ -501,7 +540,7 @@ export class SequencerPublisher {
|
|
|
501
540
|
this.requests.push(request);
|
|
502
541
|
}
|
|
503
542
|
getCurrentL2Slot() {
|
|
504
|
-
return this.epochCache.
|
|
543
|
+
return this.epochCache.getSlotNow();
|
|
505
544
|
}
|
|
506
545
|
/**
|
|
507
546
|
* Clears all pending requests without sending them.
|
|
@@ -590,8 +629,8 @@ export class SequencerPublisher {
|
|
|
590
629
|
// @note - we can only have one blob config per bundle
|
|
591
630
|
// find requests with gas and blob configs
|
|
592
631
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
593
|
-
const gasConfigs =
|
|
594
|
-
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);
|
|
595
634
|
if (blobConfigs.length > 1) {
|
|
596
635
|
throw new Error('Multiple blob configs found');
|
|
597
636
|
}
|
|
@@ -618,12 +657,34 @@ export class SequencerPublisher {
|
|
|
618
657
|
// This ensures the committee gets precomputed correctly
|
|
619
658
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
620
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
|
+
};
|
|
621
679
|
this.log.debug('Forwarding transactions', {
|
|
622
680
|
validRequests: validRequests.map((request)=>request.action),
|
|
623
681
|
txConfig
|
|
624
682
|
});
|
|
625
|
-
const result = await
|
|
626
|
-
|
|
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);
|
|
627
688
|
return {
|
|
628
689
|
result,
|
|
629
690
|
expiredActions,
|
|
@@ -643,17 +704,118 @@ export class SequencerPublisher {
|
|
|
643
704
|
}
|
|
644
705
|
}
|
|
645
706
|
}
|
|
646
|
-
|
|
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) {
|
|
647
777
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
648
778
|
if (result instanceof FormattedViemError) {
|
|
649
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
|
+
});
|
|
650
803
|
return {
|
|
651
804
|
failedActions: requests.map((r)=>r.action)
|
|
652
805
|
};
|
|
653
806
|
} else {
|
|
654
807
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
655
808
|
result,
|
|
656
|
-
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
|
+
}))
|
|
657
819
|
});
|
|
658
820
|
const successfulActions = [];
|
|
659
821
|
const failedActions = [];
|
|
@@ -664,6 +826,37 @@ export class SequencerPublisher {
|
|
|
664
826
|
failedActions.push(request.action);
|
|
665
827
|
}
|
|
666
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
|
+
}
|
|
667
860
|
return {
|
|
668
861
|
successfulActions,
|
|
669
862
|
failedActions
|
|
@@ -671,19 +864,20 @@ export class SequencerPublisher {
|
|
|
671
864
|
}
|
|
672
865
|
}
|
|
673
866
|
/**
|
|
674
|
-
* @notice Will call `
|
|
867
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
675
868
|
* @param tipArchive - The archive to check
|
|
676
869
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
677
|
-
*/
|
|
870
|
+
*/ async canProposeAt(tipArchive, msgSender, simulationOverridesPlan) {
|
|
678
871
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
679
872
|
const ignoredErrors = [
|
|
680
873
|
'SlotAlreadyInChain',
|
|
681
874
|
'InvalidProposer',
|
|
682
875
|
'InvalidArchive'
|
|
683
876
|
];
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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)=>{
|
|
687
881
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
688
882
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
689
883
|
error: err.message
|
|
@@ -699,7 +893,7 @@ export class SequencerPublisher {
|
|
|
699
893
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
700
894
|
* It will throw if the block header is invalid.
|
|
701
895
|
* @param header - The block header to validate
|
|
702
|
-
*/ async validateBlockHeader(header,
|
|
896
|
+
*/ async validateBlockHeader(header, simulationOverridesPlan) {
|
|
703
897
|
const flags = {
|
|
704
898
|
ignoreDA: true,
|
|
705
899
|
ignoreSignatures: true
|
|
@@ -713,8 +907,8 @@ export class SequencerPublisher {
|
|
|
713
907
|
header.blobsHash.toString(),
|
|
714
908
|
flags
|
|
715
909
|
];
|
|
716
|
-
const ts =
|
|
717
|
-
const stateOverrides = await this.rollupContract
|
|
910
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
911
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
718
912
|
let balance = 0n;
|
|
719
913
|
if (this.config.fishermanMode) {
|
|
720
914
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -736,7 +930,7 @@ export class SequencerPublisher {
|
|
|
736
930
|
}),
|
|
737
931
|
from: MULTI_CALL_3_ADDRESS
|
|
738
932
|
}, {
|
|
739
|
-
time: ts
|
|
933
|
+
time: ts
|
|
740
934
|
}, stateOverrides);
|
|
741
935
|
this.log.debug(`Simulated validateHeader`);
|
|
742
936
|
}
|
|
@@ -766,6 +960,7 @@ export class SequencerPublisher {
|
|
|
766
960
|
...logData,
|
|
767
961
|
request
|
|
768
962
|
});
|
|
963
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
769
964
|
try {
|
|
770
965
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
771
966
|
request.abi ?? [],
|
|
@@ -781,6 +976,7 @@ export class SequencerPublisher {
|
|
|
781
976
|
gasUsed,
|
|
782
977
|
checkpointNumber,
|
|
783
978
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
979
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
784
980
|
reason
|
|
785
981
|
};
|
|
786
982
|
} catch (err) {
|
|
@@ -793,8 +989,8 @@ export class SequencerPublisher {
|
|
|
793
989
|
request,
|
|
794
990
|
error: viemError.message
|
|
795
991
|
});
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
992
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
993
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
798
994
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
|
|
799
995
|
...logData
|
|
800
996
|
});
|
|
@@ -808,6 +1004,27 @@ export class SequencerPublisher {
|
|
|
808
1004
|
}
|
|
809
1005
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
810
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
|
+
});
|
|
811
1028
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
812
1029
|
cause: viemError
|
|
813
1030
|
});
|
|
@@ -833,31 +1050,16 @@ export class SequencerPublisher {
|
|
|
833
1050
|
throw new Error(`Unknown reason for invalidation`);
|
|
834
1051
|
}
|
|
835
1052
|
}
|
|
836
|
-
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature,
|
|
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
|
-
// }
|
|
1053
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan) {
|
|
852
1054
|
const blobFields = checkpoint.toBlobFields();
|
|
853
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1055
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
854
1056
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
855
1057
|
const args = [
|
|
856
1058
|
{
|
|
857
1059
|
header: checkpoint.header.toViem(),
|
|
858
1060
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
859
1061
|
oracleInput: {
|
|
860
|
-
feeAssetPriceModifier:
|
|
1062
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
861
1063
|
}
|
|
862
1064
|
},
|
|
863
1065
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -865,10 +1067,9 @@ export class SequencerPublisher {
|
|
|
865
1067
|
attestationsAndSignersSignature.toViemSignature(),
|
|
866
1068
|
blobInput
|
|
867
1069
|
];
|
|
868
|
-
await this.simulateProposeTx(args,
|
|
869
|
-
return ts;
|
|
1070
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
870
1071
|
}
|
|
871
|
-
async enqueueCastSignalHelper(slotNumber,
|
|
1072
|
+
async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
|
|
872
1073
|
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
873
1074
|
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
874
1075
|
return false;
|
|
@@ -892,6 +1093,28 @@ export class SequencerPublisher {
|
|
|
892
1093
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
893
1094
|
return false;
|
|
894
1095
|
}
|
|
1096
|
+
// Check if payload was already submitted to governance
|
|
1097
|
+
const cacheKey = payload.toString();
|
|
1098
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
1099
|
+
try {
|
|
1100
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
1101
|
+
const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
|
|
1102
|
+
0,
|
|
1103
|
+
1,
|
|
1104
|
+
2
|
|
1105
|
+
]), this.log, true);
|
|
1106
|
+
if (proposed) {
|
|
1107
|
+
this.payloadProposedCache.add(cacheKey);
|
|
1108
|
+
}
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
1115
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
895
1118
|
const cachedLastVote = this.lastActions[signalType];
|
|
896
1119
|
this.lastActions[signalType] = slotNumber;
|
|
897
1120
|
const action = signalType;
|
|
@@ -902,6 +1125,8 @@ export class SequencerPublisher {
|
|
|
902
1125
|
signer: this.l1TxUtils.client.account?.address,
|
|
903
1126
|
lastValidL2Slot: slotNumber
|
|
904
1127
|
});
|
|
1128
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1129
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
905
1130
|
try {
|
|
906
1131
|
await this.l1TxUtils.simulate(request, {
|
|
907
1132
|
time: timestamp
|
|
@@ -913,7 +1138,32 @@ export class SequencerPublisher {
|
|
|
913
1138
|
request
|
|
914
1139
|
});
|
|
915
1140
|
} catch (err) {
|
|
916
|
-
|
|
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
|
+
});
|
|
917
1167
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
918
1168
|
}
|
|
919
1169
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -957,55 +1207,17 @@ export class SequencerPublisher {
|
|
|
957
1207
|
/**
|
|
958
1208
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
959
1209
|
* @param slotNumber - The slot number to cast a signal for.
|
|
960
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
961
1210
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
962
|
-
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber,
|
|
963
|
-
return this.enqueueCastSignalHelper(slotNumber,
|
|
1211
|
+
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
|
|
1212
|
+
return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
964
1213
|
}
|
|
965
|
-
/** 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) {
|
|
966
1215
|
if (actions.length === 0) {
|
|
967
1216
|
this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
|
|
968
1217
|
return false;
|
|
969
1218
|
}
|
|
970
1219
|
for (const action of actions){
|
|
971
1220
|
switch(action.type){
|
|
972
|
-
case 'vote-empire-payload':
|
|
973
|
-
{
|
|
974
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
975
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
979
|
-
signerAddress
|
|
980
|
-
});
|
|
981
|
-
await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
|
|
982
|
-
break;
|
|
983
|
-
}
|
|
984
|
-
case 'create-empire-payload':
|
|
985
|
-
{
|
|
986
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, {
|
|
987
|
-
slotNumber,
|
|
988
|
-
signerAddress
|
|
989
|
-
});
|
|
990
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
991
|
-
await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, timestamp);
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
case 'execute-empire-payload':
|
|
995
|
-
{
|
|
996
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, {
|
|
997
|
-
slotNumber,
|
|
998
|
-
signerAddress
|
|
999
|
-
});
|
|
1000
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1001
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
1002
|
-
return false;
|
|
1003
|
-
}
|
|
1004
|
-
const empireSlashingProposer = this.slashingProposerContract;
|
|
1005
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1006
|
-
await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, timestamp);
|
|
1007
|
-
break;
|
|
1008
|
-
}
|
|
1009
1221
|
case 'vote-offenses':
|
|
1010
1222
|
{
|
|
1011
1223
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
@@ -1014,14 +1226,13 @@ export class SequencerPublisher {
|
|
|
1014
1226
|
votesCount: action.votes.length,
|
|
1015
1227
|
signerAddress
|
|
1016
1228
|
});
|
|
1017
|
-
if (this.slashingProposerContract
|
|
1018
|
-
this.log.error('
|
|
1229
|
+
if (!this.slashingProposerContract) {
|
|
1230
|
+
this.log.error('No slashing proposer contract available');
|
|
1019
1231
|
return false;
|
|
1020
1232
|
}
|
|
1021
|
-
const tallySlashingProposer = this.slashingProposerContract;
|
|
1022
1233
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1023
|
-
const request = await
|
|
1024
|
-
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);
|
|
1025
1236
|
break;
|
|
1026
1237
|
}
|
|
1027
1238
|
case 'execute-slash':
|
|
@@ -1031,13 +1242,12 @@ export class SequencerPublisher {
|
|
|
1031
1242
|
round: action.round,
|
|
1032
1243
|
signerAddress
|
|
1033
1244
|
});
|
|
1034
|
-
if (this.slashingProposerContract
|
|
1035
|
-
this.log.error('
|
|
1245
|
+
if (!this.slashingProposerContract) {
|
|
1246
|
+
this.log.error('No slashing proposer contract available');
|
|
1036
1247
|
return false;
|
|
1037
1248
|
}
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
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);
|
|
1041
1251
|
break;
|
|
1042
1252
|
}
|
|
1043
1253
|
default:
|
|
@@ -1052,35 +1262,49 @@ export class SequencerPublisher {
|
|
|
1052
1262
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1053
1263
|
const checkpointHeader = checkpoint.header;
|
|
1054
1264
|
const blobFields = checkpoint.toBlobFields();
|
|
1055
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1265
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1056
1266
|
const proposeTxArgs = {
|
|
1057
1267
|
header: checkpointHeader,
|
|
1058
1268
|
archive: checkpoint.archive.root.toBuffer(),
|
|
1059
1269
|
blobs,
|
|
1060
1270
|
attestationsAndSigners,
|
|
1061
|
-
attestationsAndSignersSignature
|
|
1271
|
+
attestationsAndSignersSignature,
|
|
1272
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1062
1273
|
};
|
|
1063
|
-
|
|
1274
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan).withoutBlobCheck().build();
|
|
1064
1275
|
try {
|
|
1065
1276
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
1066
1277
|
// This means that we can avoid the simulation issues in later checks.
|
|
1067
1278
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1068
1279
|
// make time consistency checks break.
|
|
1069
1280
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1070
|
-
|
|
1281
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
|
|
1071
1282
|
} catch (err) {
|
|
1072
1283
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1073
1284
|
...checkpoint.getStats(),
|
|
1074
1285
|
slotNumber: checkpoint.header.slotNumber,
|
|
1075
|
-
|
|
1286
|
+
simulationOverridesPlan
|
|
1076
1287
|
});
|
|
1077
1288
|
throw err;
|
|
1078
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
|
+
}
|
|
1079
1299
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1080
1300
|
...checkpoint.toCheckpointInfo(),
|
|
1081
|
-
|
|
1301
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1302
|
+
simulationOverridesPlan
|
|
1082
1303
|
});
|
|
1083
|
-
await this.addProposeTx(checkpoint, proposeTxArgs,
|
|
1304
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, {
|
|
1305
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1306
|
+
simulationOverridesPlan
|
|
1307
|
+
}, preCheck);
|
|
1084
1308
|
}
|
|
1085
1309
|
enqueueInvalidateCheckpoint(request, opts = {}) {
|
|
1086
1310
|
if (!request) {
|
|
@@ -1121,7 +1345,8 @@ export class SequencerPublisher {
|
|
|
1121
1345
|
}
|
|
1122
1346
|
});
|
|
1123
1347
|
}
|
|
1124
|
-
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber
|
|
1348
|
+
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
|
|
1349
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1125
1350
|
const logData = {
|
|
1126
1351
|
slotNumber,
|
|
1127
1352
|
timestamp,
|
|
@@ -1134,6 +1359,7 @@ export class SequencerPublisher {
|
|
|
1134
1359
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1135
1360
|
this.lastActions[action] = slotNumber;
|
|
1136
1361
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1362
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1137
1363
|
let gasUsed;
|
|
1138
1364
|
const simulateAbi = mergeAbis([
|
|
1139
1365
|
request.abi ?? [],
|
|
@@ -1142,7 +1368,7 @@ export class SequencerPublisher {
|
|
|
1142
1368
|
try {
|
|
1143
1369
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1144
1370
|
time: timestamp
|
|
1145
|
-
}, [], simulateAbi));
|
|
1371
|
+
}, [], simulateAbi));
|
|
1146
1372
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1147
1373
|
...logData,
|
|
1148
1374
|
request,
|
|
@@ -1151,6 +1377,27 @@ export class SequencerPublisher {
|
|
|
1151
1377
|
} catch (err) {
|
|
1152
1378
|
const viemError = formatViemError(err, simulateAbi);
|
|
1153
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
|
+
});
|
|
1154
1401
|
return false;
|
|
1155
1402
|
}
|
|
1156
1403
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
@@ -1196,13 +1443,14 @@ export class SequencerPublisher {
|
|
|
1196
1443
|
* A call to `restart` is required before you can continue publishing.
|
|
1197
1444
|
*/ interrupt() {
|
|
1198
1445
|
this.interrupted = true;
|
|
1446
|
+
this.interruptibleSleep.interrupt();
|
|
1199
1447
|
this.l1TxUtils.interrupt();
|
|
1200
1448
|
}
|
|
1201
1449
|
/** Restarts the publisher after calling `interrupt`. */ restart() {
|
|
1202
1450
|
this.interrupted = false;
|
|
1203
1451
|
this.l1TxUtils.restart();
|
|
1204
1452
|
}
|
|
1205
|
-
async prepareProposeTx(encodedData,
|
|
1453
|
+
async prepareProposeTx(encodedData, simulationOverridesPlan) {
|
|
1206
1454
|
const kzg = Blob.getViemKzgInstance();
|
|
1207
1455
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1208
1456
|
this.log.debug('Validating blob input', {
|
|
@@ -1229,10 +1477,38 @@ export class SequencerPublisher {
|
|
|
1229
1477
|
}, {}, {
|
|
1230
1478
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1231
1479
|
kzg
|
|
1232
|
-
}).catch((err)=>{
|
|
1233
|
-
const
|
|
1234
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1235
|
-
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
|
+
}
|
|
1236
1512
|
});
|
|
1237
1513
|
throw new Error('Failed to validate blobs');
|
|
1238
1514
|
});
|
|
@@ -1243,8 +1519,7 @@ export class SequencerPublisher {
|
|
|
1243
1519
|
header: encodedData.header.toViem(),
|
|
1244
1520
|
archive: toHex(encodedData.archive),
|
|
1245
1521
|
oracleInput: {
|
|
1246
|
-
|
|
1247
|
-
feeAssetPriceModifier: 0n
|
|
1522
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier
|
|
1248
1523
|
}
|
|
1249
1524
|
},
|
|
1250
1525
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1252,7 +1527,7 @@ export class SequencerPublisher {
|
|
|
1252
1527
|
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
1253
1528
|
blobInput
|
|
1254
1529
|
];
|
|
1255
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1530
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1256
1531
|
return {
|
|
1257
1532
|
args,
|
|
1258
1533
|
blobEvaluationGas,
|
|
@@ -1263,29 +1538,14 @@ export class SequencerPublisher {
|
|
|
1263
1538
|
/**
|
|
1264
1539
|
* Simulates the propose tx with eth_simulateV1
|
|
1265
1540
|
* @param args - The propose tx args
|
|
1266
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1267
1541
|
* @returns The simulation result
|
|
1268
|
-
*/ async simulateProposeTx(args,
|
|
1542
|
+
*/ async simulateProposeTx(args, simulationOverridesPlan) {
|
|
1269
1543
|
const rollupData = encodeFunctionData({
|
|
1270
1544
|
abi: RollupAbi,
|
|
1271
1545
|
functionName: 'propose',
|
|
1272
1546
|
args
|
|
1273
1547
|
});
|
|
1274
|
-
|
|
1275
|
-
const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1276
|
-
const stateOverrides = [
|
|
1277
|
-
{
|
|
1278
|
-
address: this.rollupContract.address,
|
|
1279
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1280
|
-
stateDiff: [
|
|
1281
|
-
{
|
|
1282
|
-
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
1283
|
-
value: toPaddedHex(0n, true)
|
|
1284
|
-
},
|
|
1285
|
-
...forcePendingCheckpointNumberStateDiff
|
|
1286
|
-
]
|
|
1287
|
-
}
|
|
1288
|
-
];
|
|
1548
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1289
1549
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1290
1550
|
if (this.proposerAddressForSimulation) {
|
|
1291
1551
|
stateOverrides.push({
|
|
@@ -1293,6 +1553,8 @@ export class SequencerPublisher {
|
|
|
1293
1553
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1294
1554
|
});
|
|
1295
1555
|
}
|
|
1556
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1557
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1296
1558
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1297
1559
|
to: this.rollupContract.address,
|
|
1298
1560
|
data: rollupData,
|
|
@@ -1301,8 +1563,7 @@ export class SequencerPublisher {
|
|
|
1301
1563
|
from: this.proposerAddressForSimulation.toString()
|
|
1302
1564
|
}
|
|
1303
1565
|
}, {
|
|
1304
|
-
|
|
1305
|
-
time: timestamp + 1n,
|
|
1566
|
+
time: simTs,
|
|
1306
1567
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1307
1568
|
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1308
1569
|
}, stateOverrides, RollupAbi, {
|
|
@@ -1319,7 +1580,29 @@ export class SequencerPublisher {
|
|
|
1319
1580
|
logs: []
|
|
1320
1581
|
};
|
|
1321
1582
|
}
|
|
1322
|
-
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
|
+
});
|
|
1323
1606
|
throw err;
|
|
1324
1607
|
});
|
|
1325
1608
|
return {
|
|
@@ -1327,11 +1610,11 @@ export class SequencerPublisher {
|
|
|
1327
1610
|
simulationResult
|
|
1328
1611
|
};
|
|
1329
1612
|
}
|
|
1330
|
-
async addProposeTx(checkpoint, encodedData, opts = {},
|
|
1613
|
+
async addProposeTx(checkpoint, encodedData, opts = {}, preCheck) {
|
|
1331
1614
|
const slot = checkpoint.header.slotNumber;
|
|
1332
1615
|
const timer = new Timer();
|
|
1333
1616
|
const kzg = Blob.getViemKzgInstance();
|
|
1334
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData,
|
|
1617
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts.simulationOverridesPlan);
|
|
1335
1618
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1336
1619
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
1337
1620
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
@@ -1347,9 +1630,10 @@ export class SequencerPublisher {
|
|
|
1347
1630
|
},
|
|
1348
1631
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1349
1632
|
gasConfig: {
|
|
1350
|
-
|
|
1633
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1351
1634
|
gasLimit
|
|
1352
1635
|
},
|
|
1636
|
+
preCheck,
|
|
1353
1637
|
blobConfig: {
|
|
1354
1638
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1355
1639
|
kzg
|
|
@@ -1396,4 +1680,13 @@ export class SequencerPublisher {
|
|
|
1396
1680
|
}
|
|
1397
1681
|
});
|
|
1398
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
|
+
}
|
|
1399
1692
|
}
|