@aztec/sequencer-client 0.0.1-commit.0c875d939 → 0.0.1-commit.0dc957cde
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 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +49 -29
- 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 +68 -48
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +419 -135
- 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 +40 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +522 -212
- 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 +7 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +18 -8
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +85 -21
- package/dest/sequencer/sequencer.d.ts +43 -17
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +158 -97
- 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/index.d.ts +3 -5
- package/dest/test/index.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 +81 -30
- package/src/config.ts +61 -35
- 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 +472 -193
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +665 -234
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +6 -1
- package/src/sequencer/metrics.ts +100 -26
- package/src/sequencer/sequencer.ts +227 -109
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +51 -48
|
@@ -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 { 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';
|
|
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,16 +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;
|
|
441
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;
|
|
442
450
|
// A CALL to a cold address is 2700 gas
|
|
443
451
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
444
452
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -447,7 +455,6 @@ export class SequencerPublisher {
|
|
|
447
455
|
rollupContract;
|
|
448
456
|
govProposerContract;
|
|
449
457
|
slashingProposerContract;
|
|
450
|
-
slashFactoryContract;
|
|
451
458
|
tracer;
|
|
452
459
|
requests;
|
|
453
460
|
constructor(config, deps){
|
|
@@ -457,16 +464,22 @@ export class SequencerPublisher {
|
|
|
457
464
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
458
465
|
this.lastActions = {};
|
|
459
466
|
this.isPayloadEmptyCache = new Map();
|
|
467
|
+
this.payloadProposedCache = new Set();
|
|
468
|
+
this.interruptibleSleep = new InterruptibleSleep();
|
|
460
469
|
this.requests = [];
|
|
461
470
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
462
471
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
472
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
473
|
+
this.dateProvider = deps.dateProvider;
|
|
463
474
|
this.epochCache = deps.epochCache;
|
|
464
475
|
this.lastActions = deps.lastActions;
|
|
465
476
|
this.blobClient = deps.blobClient;
|
|
477
|
+
this.dateProvider = deps.dateProvider;
|
|
466
478
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
467
479
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
468
480
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
469
481
|
this.l1TxUtils = deps.l1TxUtils;
|
|
482
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
470
483
|
this.rollupContract = deps.rollupContract;
|
|
471
484
|
this.govProposerContract = deps.governanceProposerContract;
|
|
472
485
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -475,13 +488,30 @@ export class SequencerPublisher {
|
|
|
475
488
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
476
489
|
this.slashingProposerContract = newSlashingProposer;
|
|
477
490
|
});
|
|
478
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
479
491
|
// Initialize L1 fee analyzer for fisherman mode
|
|
480
492
|
if (config.fishermanMode) {
|
|
481
493
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
482
494
|
}
|
|
483
495
|
// Initialize fee asset price oracle
|
|
484
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
|
+
});
|
|
485
515
|
}
|
|
486
516
|
getRollupContract() {
|
|
487
517
|
return this.rollupContract;
|
|
@@ -510,7 +540,7 @@ export class SequencerPublisher {
|
|
|
510
540
|
this.requests.push(request);
|
|
511
541
|
}
|
|
512
542
|
getCurrentL2Slot() {
|
|
513
|
-
return this.epochCache.
|
|
543
|
+
return this.epochCache.getSlotNow();
|
|
514
544
|
}
|
|
515
545
|
/**
|
|
516
546
|
* Clears all pending requests without sending them.
|
|
@@ -599,8 +629,8 @@ export class SequencerPublisher {
|
|
|
599
629
|
// @note - we can only have one blob config per bundle
|
|
600
630
|
// find requests with gas and blob configs
|
|
601
631
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
602
|
-
const gasConfigs =
|
|
603
|
-
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);
|
|
604
634
|
if (blobConfigs.length > 1) {
|
|
605
635
|
throw new Error('Multiple blob configs found');
|
|
606
636
|
}
|
|
@@ -627,12 +657,34 @@ export class SequencerPublisher {
|
|
|
627
657
|
// This ensures the committee gets precomputed correctly
|
|
628
658
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
629
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
|
+
};
|
|
630
679
|
this.log.debug('Forwarding transactions', {
|
|
631
680
|
validRequests: validRequests.map((request)=>request.action),
|
|
632
681
|
txConfig
|
|
633
682
|
});
|
|
634
|
-
const result = await
|
|
635
|
-
|
|
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);
|
|
636
688
|
return {
|
|
637
689
|
result,
|
|
638
690
|
expiredActions,
|
|
@@ -652,17 +704,118 @@ export class SequencerPublisher {
|
|
|
652
704
|
}
|
|
653
705
|
}
|
|
654
706
|
}
|
|
655
|
-
|
|
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) {
|
|
656
777
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
657
778
|
if (result instanceof FormattedViemError) {
|
|
658
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
|
+
});
|
|
659
803
|
return {
|
|
660
804
|
failedActions: requests.map((r)=>r.action)
|
|
661
805
|
};
|
|
662
806
|
} else {
|
|
663
807
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
664
808
|
result,
|
|
665
|
-
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
|
+
}))
|
|
666
819
|
});
|
|
667
820
|
const successfulActions = [];
|
|
668
821
|
const failedActions = [];
|
|
@@ -673,6 +826,37 @@ export class SequencerPublisher {
|
|
|
673
826
|
failedActions.push(request.action);
|
|
674
827
|
}
|
|
675
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
|
+
}
|
|
676
860
|
return {
|
|
677
861
|
successfulActions,
|
|
678
862
|
failedActions
|
|
@@ -680,19 +864,20 @@ export class SequencerPublisher {
|
|
|
680
864
|
}
|
|
681
865
|
}
|
|
682
866
|
/**
|
|
683
|
-
* @notice Will call `
|
|
867
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
684
868
|
* @param tipArchive - The archive to check
|
|
685
869
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
686
|
-
*/
|
|
870
|
+
*/ async canProposeAt(tipArchive, msgSender, simulationOverridesPlan) {
|
|
687
871
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
688
872
|
const ignoredErrors = [
|
|
689
873
|
'SlotAlreadyInChain',
|
|
690
874
|
'InvalidProposer',
|
|
691
875
|
'InvalidArchive'
|
|
692
876
|
];
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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)=>{
|
|
696
881
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
697
882
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
698
883
|
error: err.message
|
|
@@ -708,7 +893,7 @@ export class SequencerPublisher {
|
|
|
708
893
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
709
894
|
* It will throw if the block header is invalid.
|
|
710
895
|
* @param header - The block header to validate
|
|
711
|
-
*/ async validateBlockHeader(header,
|
|
896
|
+
*/ async validateBlockHeader(header, simulationOverridesPlan) {
|
|
712
897
|
const flags = {
|
|
713
898
|
ignoreDA: true,
|
|
714
899
|
ignoreSignatures: true
|
|
@@ -722,8 +907,8 @@ export class SequencerPublisher {
|
|
|
722
907
|
header.blobsHash.toString(),
|
|
723
908
|
flags
|
|
724
909
|
];
|
|
725
|
-
const ts =
|
|
726
|
-
const stateOverrides = await this.rollupContract
|
|
910
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
911
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
727
912
|
let balance = 0n;
|
|
728
913
|
if (this.config.fishermanMode) {
|
|
729
914
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -745,7 +930,7 @@ export class SequencerPublisher {
|
|
|
745
930
|
}),
|
|
746
931
|
from: MULTI_CALL_3_ADDRESS
|
|
747
932
|
}, {
|
|
748
|
-
time: ts
|
|
933
|
+
time: ts
|
|
749
934
|
}, stateOverrides);
|
|
750
935
|
this.log.debug(`Simulated validateHeader`);
|
|
751
936
|
}
|
|
@@ -775,6 +960,7 @@ export class SequencerPublisher {
|
|
|
775
960
|
...logData,
|
|
776
961
|
request
|
|
777
962
|
});
|
|
963
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
778
964
|
try {
|
|
779
965
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
780
966
|
request.abi ?? [],
|
|
@@ -790,6 +976,7 @@ export class SequencerPublisher {
|
|
|
790
976
|
gasUsed,
|
|
791
977
|
checkpointNumber,
|
|
792
978
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
979
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
793
980
|
reason
|
|
794
981
|
};
|
|
795
982
|
} catch (err) {
|
|
@@ -802,8 +989,8 @@ export class SequencerPublisher {
|
|
|
802
989
|
request,
|
|
803
990
|
error: viemError.message
|
|
804
991
|
});
|
|
805
|
-
const
|
|
806
|
-
if (
|
|
992
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
993
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
807
994
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
|
|
808
995
|
...logData
|
|
809
996
|
});
|
|
@@ -817,6 +1004,27 @@ export class SequencerPublisher {
|
|
|
817
1004
|
}
|
|
818
1005
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
819
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
|
+
});
|
|
820
1028
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
821
1029
|
cause: viemError
|
|
822
1030
|
});
|
|
@@ -842,24 +1050,9 @@ export class SequencerPublisher {
|
|
|
842
1050
|
throw new Error(`Unknown reason for invalidation`);
|
|
843
1051
|
}
|
|
844
1052
|
}
|
|
845
|
-
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature,
|
|
846
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
847
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
848
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
849
|
-
// so that the committee is recalculated correctly
|
|
850
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
851
|
-
// if (ignoreSignatures) {
|
|
852
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
853
|
-
// if (!committee) {
|
|
854
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
855
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
856
|
-
// }
|
|
857
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
858
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
859
|
-
// );
|
|
860
|
-
// }
|
|
1053
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan) {
|
|
861
1054
|
const blobFields = checkpoint.toBlobFields();
|
|
862
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1055
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
863
1056
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
864
1057
|
const args = [
|
|
865
1058
|
{
|
|
@@ -874,10 +1067,9 @@ export class SequencerPublisher {
|
|
|
874
1067
|
attestationsAndSignersSignature.toViemSignature(),
|
|
875
1068
|
blobInput
|
|
876
1069
|
];
|
|
877
|
-
await this.simulateProposeTx(args,
|
|
878
|
-
return ts;
|
|
1070
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
879
1071
|
}
|
|
880
|
-
async enqueueCastSignalHelper(slotNumber,
|
|
1072
|
+
async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
|
|
881
1073
|
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
882
1074
|
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
883
1075
|
return false;
|
|
@@ -901,6 +1093,28 @@ export class SequencerPublisher {
|
|
|
901
1093
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
902
1094
|
return false;
|
|
903
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
|
+
}
|
|
904
1118
|
const cachedLastVote = this.lastActions[signalType];
|
|
905
1119
|
this.lastActions[signalType] = slotNumber;
|
|
906
1120
|
const action = signalType;
|
|
@@ -911,6 +1125,8 @@ export class SequencerPublisher {
|
|
|
911
1125
|
signer: this.l1TxUtils.client.account?.address,
|
|
912
1126
|
lastValidL2Slot: slotNumber
|
|
913
1127
|
});
|
|
1128
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1129
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
914
1130
|
try {
|
|
915
1131
|
await this.l1TxUtils.simulate(request, {
|
|
916
1132
|
time: timestamp
|
|
@@ -922,7 +1138,32 @@ export class SequencerPublisher {
|
|
|
922
1138
|
request
|
|
923
1139
|
});
|
|
924
1140
|
} catch (err) {
|
|
925
|
-
|
|
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
|
+
});
|
|
926
1167
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
927
1168
|
}
|
|
928
1169
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -966,55 +1207,17 @@ export class SequencerPublisher {
|
|
|
966
1207
|
/**
|
|
967
1208
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
968
1209
|
* @param slotNumber - The slot number to cast a signal for.
|
|
969
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
970
1210
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
971
|
-
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber,
|
|
972
|
-
return this.enqueueCastSignalHelper(slotNumber,
|
|
1211
|
+
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
|
|
1212
|
+
return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
973
1213
|
}
|
|
974
|
-
/** 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) {
|
|
975
1215
|
if (actions.length === 0) {
|
|
976
1216
|
this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
|
|
977
1217
|
return false;
|
|
978
1218
|
}
|
|
979
1219
|
for (const action of actions){
|
|
980
1220
|
switch(action.type){
|
|
981
|
-
case 'vote-empire-payload':
|
|
982
|
-
{
|
|
983
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
984
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
985
|
-
break;
|
|
986
|
-
}
|
|
987
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
988
|
-
signerAddress
|
|
989
|
-
});
|
|
990
|
-
await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
|
|
991
|
-
break;
|
|
992
|
-
}
|
|
993
|
-
case 'create-empire-payload':
|
|
994
|
-
{
|
|
995
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, {
|
|
996
|
-
slotNumber,
|
|
997
|
-
signerAddress
|
|
998
|
-
});
|
|
999
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
1000
|
-
await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, timestamp);
|
|
1001
|
-
break;
|
|
1002
|
-
}
|
|
1003
|
-
case 'execute-empire-payload':
|
|
1004
|
-
{
|
|
1005
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, {
|
|
1006
|
-
slotNumber,
|
|
1007
|
-
signerAddress
|
|
1008
|
-
});
|
|
1009
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1010
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
1011
|
-
return false;
|
|
1012
|
-
}
|
|
1013
|
-
const empireSlashingProposer = this.slashingProposerContract;
|
|
1014
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1015
|
-
await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, timestamp);
|
|
1016
|
-
break;
|
|
1017
|
-
}
|
|
1018
1221
|
case 'vote-offenses':
|
|
1019
1222
|
{
|
|
1020
1223
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
@@ -1023,14 +1226,13 @@ export class SequencerPublisher {
|
|
|
1023
1226
|
votesCount: action.votes.length,
|
|
1024
1227
|
signerAddress
|
|
1025
1228
|
});
|
|
1026
|
-
if (this.slashingProposerContract
|
|
1027
|
-
this.log.error('
|
|
1229
|
+
if (!this.slashingProposerContract) {
|
|
1230
|
+
this.log.error('No slashing proposer contract available');
|
|
1028
1231
|
return false;
|
|
1029
1232
|
}
|
|
1030
|
-
const tallySlashingProposer = this.slashingProposerContract;
|
|
1031
1233
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1032
|
-
const request = await
|
|
1033
|
-
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);
|
|
1034
1236
|
break;
|
|
1035
1237
|
}
|
|
1036
1238
|
case 'execute-slash':
|
|
@@ -1040,13 +1242,12 @@ export class SequencerPublisher {
|
|
|
1040
1242
|
round: action.round,
|
|
1041
1243
|
signerAddress
|
|
1042
1244
|
});
|
|
1043
|
-
if (this.slashingProposerContract
|
|
1044
|
-
this.log.error('
|
|
1245
|
+
if (!this.slashingProposerContract) {
|
|
1246
|
+
this.log.error('No slashing proposer contract available');
|
|
1045
1247
|
return false;
|
|
1046
1248
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
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);
|
|
1050
1251
|
break;
|
|
1051
1252
|
}
|
|
1052
1253
|
default:
|
|
@@ -1061,7 +1262,7 @@ export class SequencerPublisher {
|
|
|
1061
1262
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1062
1263
|
const checkpointHeader = checkpoint.header;
|
|
1063
1264
|
const blobFields = checkpoint.toBlobFields();
|
|
1064
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1265
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1065
1266
|
const proposeTxArgs = {
|
|
1066
1267
|
header: checkpointHeader,
|
|
1067
1268
|
archive: checkpoint.archive.root.toBuffer(),
|
|
@@ -1070,27 +1271,40 @@ export class SequencerPublisher {
|
|
|
1070
1271
|
attestationsAndSignersSignature,
|
|
1071
1272
|
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1072
1273
|
};
|
|
1073
|
-
|
|
1274
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan).withoutBlobCheck().build();
|
|
1074
1275
|
try {
|
|
1075
1276
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
1076
1277
|
// This means that we can avoid the simulation issues in later checks.
|
|
1077
1278
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1078
1279
|
// make time consistency checks break.
|
|
1079
1280
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1080
|
-
|
|
1281
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
|
|
1081
1282
|
} catch (err) {
|
|
1082
1283
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1083
1284
|
...checkpoint.getStats(),
|
|
1084
1285
|
slotNumber: checkpoint.header.slotNumber,
|
|
1085
|
-
|
|
1286
|
+
simulationOverridesPlan
|
|
1086
1287
|
});
|
|
1087
1288
|
throw err;
|
|
1088
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
|
+
}
|
|
1089
1299
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1090
1300
|
...checkpoint.toCheckpointInfo(),
|
|
1091
|
-
|
|
1301
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1302
|
+
simulationOverridesPlan
|
|
1092
1303
|
});
|
|
1093
|
-
await this.addProposeTx(checkpoint, proposeTxArgs,
|
|
1304
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, {
|
|
1305
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1306
|
+
simulationOverridesPlan
|
|
1307
|
+
}, preCheck);
|
|
1094
1308
|
}
|
|
1095
1309
|
enqueueInvalidateCheckpoint(request, opts = {}) {
|
|
1096
1310
|
if (!request) {
|
|
@@ -1131,7 +1345,8 @@ export class SequencerPublisher {
|
|
|
1131
1345
|
}
|
|
1132
1346
|
});
|
|
1133
1347
|
}
|
|
1134
|
-
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber
|
|
1348
|
+
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
|
|
1349
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1135
1350
|
const logData = {
|
|
1136
1351
|
slotNumber,
|
|
1137
1352
|
timestamp,
|
|
@@ -1144,6 +1359,7 @@ export class SequencerPublisher {
|
|
|
1144
1359
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1145
1360
|
this.lastActions[action] = slotNumber;
|
|
1146
1361
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1362
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1147
1363
|
let gasUsed;
|
|
1148
1364
|
const simulateAbi = mergeAbis([
|
|
1149
1365
|
request.abi ?? [],
|
|
@@ -1152,7 +1368,7 @@ export class SequencerPublisher {
|
|
|
1152
1368
|
try {
|
|
1153
1369
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1154
1370
|
time: timestamp
|
|
1155
|
-
}, [], simulateAbi));
|
|
1371
|
+
}, [], simulateAbi));
|
|
1156
1372
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1157
1373
|
...logData,
|
|
1158
1374
|
request,
|
|
@@ -1161,6 +1377,27 @@ export class SequencerPublisher {
|
|
|
1161
1377
|
} catch (err) {
|
|
1162
1378
|
const viemError = formatViemError(err, simulateAbi);
|
|
1163
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
|
+
});
|
|
1164
1401
|
return false;
|
|
1165
1402
|
}
|
|
1166
1403
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
@@ -1206,13 +1443,14 @@ export class SequencerPublisher {
|
|
|
1206
1443
|
* A call to `restart` is required before you can continue publishing.
|
|
1207
1444
|
*/ interrupt() {
|
|
1208
1445
|
this.interrupted = true;
|
|
1446
|
+
this.interruptibleSleep.interrupt();
|
|
1209
1447
|
this.l1TxUtils.interrupt();
|
|
1210
1448
|
}
|
|
1211
1449
|
/** Restarts the publisher after calling `interrupt`. */ restart() {
|
|
1212
1450
|
this.interrupted = false;
|
|
1213
1451
|
this.l1TxUtils.restart();
|
|
1214
1452
|
}
|
|
1215
|
-
async prepareProposeTx(encodedData,
|
|
1453
|
+
async prepareProposeTx(encodedData, simulationOverridesPlan) {
|
|
1216
1454
|
const kzg = Blob.getViemKzgInstance();
|
|
1217
1455
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1218
1456
|
this.log.debug('Validating blob input', {
|
|
@@ -1239,10 +1477,38 @@ export class SequencerPublisher {
|
|
|
1239
1477
|
}, {}, {
|
|
1240
1478
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1241
1479
|
kzg
|
|
1242
|
-
}).catch((err)=>{
|
|
1243
|
-
const
|
|
1244
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1245
|
-
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
|
+
}
|
|
1246
1512
|
});
|
|
1247
1513
|
throw new Error('Failed to validate blobs');
|
|
1248
1514
|
});
|
|
@@ -1261,7 +1527,7 @@ export class SequencerPublisher {
|
|
|
1261
1527
|
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
1262
1528
|
blobInput
|
|
1263
1529
|
];
|
|
1264
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1530
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1265
1531
|
return {
|
|
1266
1532
|
args,
|
|
1267
1533
|
blobEvaluationGas,
|
|
@@ -1272,29 +1538,14 @@ export class SequencerPublisher {
|
|
|
1272
1538
|
/**
|
|
1273
1539
|
* Simulates the propose tx with eth_simulateV1
|
|
1274
1540
|
* @param args - The propose tx args
|
|
1275
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1276
1541
|
* @returns The simulation result
|
|
1277
|
-
*/ async simulateProposeTx(args,
|
|
1542
|
+
*/ async simulateProposeTx(args, simulationOverridesPlan) {
|
|
1278
1543
|
const rollupData = encodeFunctionData({
|
|
1279
1544
|
abi: RollupAbi,
|
|
1280
1545
|
functionName: 'propose',
|
|
1281
1546
|
args
|
|
1282
1547
|
});
|
|
1283
|
-
|
|
1284
|
-
const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1285
|
-
const stateOverrides = [
|
|
1286
|
-
{
|
|
1287
|
-
address: this.rollupContract.address,
|
|
1288
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1289
|
-
stateDiff: [
|
|
1290
|
-
{
|
|
1291
|
-
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
1292
|
-
value: toPaddedHex(0n, true)
|
|
1293
|
-
},
|
|
1294
|
-
...forcePendingCheckpointNumberStateDiff
|
|
1295
|
-
]
|
|
1296
|
-
}
|
|
1297
|
-
];
|
|
1548
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1298
1549
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1299
1550
|
if (this.proposerAddressForSimulation) {
|
|
1300
1551
|
stateOverrides.push({
|
|
@@ -1302,6 +1553,8 @@ export class SequencerPublisher {
|
|
|
1302
1553
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1303
1554
|
});
|
|
1304
1555
|
}
|
|
1556
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1557
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1305
1558
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1306
1559
|
to: this.rollupContract.address,
|
|
1307
1560
|
data: rollupData,
|
|
@@ -1310,8 +1563,7 @@ export class SequencerPublisher {
|
|
|
1310
1563
|
from: this.proposerAddressForSimulation.toString()
|
|
1311
1564
|
}
|
|
1312
1565
|
}, {
|
|
1313
|
-
|
|
1314
|
-
time: timestamp + 1n,
|
|
1566
|
+
time: simTs,
|
|
1315
1567
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1316
1568
|
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1317
1569
|
}, stateOverrides, RollupAbi, {
|
|
@@ -1328,7 +1580,29 @@ export class SequencerPublisher {
|
|
|
1328
1580
|
logs: []
|
|
1329
1581
|
};
|
|
1330
1582
|
}
|
|
1331
|
-
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
|
+
});
|
|
1332
1606
|
throw err;
|
|
1333
1607
|
});
|
|
1334
1608
|
return {
|
|
@@ -1336,11 +1610,11 @@ export class SequencerPublisher {
|
|
|
1336
1610
|
simulationResult
|
|
1337
1611
|
};
|
|
1338
1612
|
}
|
|
1339
|
-
async addProposeTx(checkpoint, encodedData, opts = {},
|
|
1613
|
+
async addProposeTx(checkpoint, encodedData, opts = {}, preCheck) {
|
|
1340
1614
|
const slot = checkpoint.header.slotNumber;
|
|
1341
1615
|
const timer = new Timer();
|
|
1342
1616
|
const kzg = Blob.getViemKzgInstance();
|
|
1343
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData,
|
|
1617
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts.simulationOverridesPlan);
|
|
1344
1618
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1345
1619
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
1346
1620
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
@@ -1356,9 +1630,10 @@ export class SequencerPublisher {
|
|
|
1356
1630
|
},
|
|
1357
1631
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1358
1632
|
gasConfig: {
|
|
1359
|
-
|
|
1633
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1360
1634
|
gasLimit
|
|
1361
1635
|
},
|
|
1636
|
+
preCheck,
|
|
1362
1637
|
blobConfig: {
|
|
1363
1638
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1364
1639
|
kzg
|
|
@@ -1405,4 +1680,13 @@ export class SequencerPublisher {
|
|
|
1405
1680
|
}
|
|
1406
1681
|
});
|
|
1407
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
|
+
}
|
|
1408
1692
|
}
|