@aztec/sequencer-client 0.0.1-commit.e2b2873ed → 0.0.1-commit.e304674f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +15 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +60 -30
- package/dest/config.d.ts +26 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +44 -21
- package/dest/global_variable_builder/global_builder.d.ts +15 -11
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +29 -25
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -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 +82 -37
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +430 -118
- package/dest/sequencer/checkpoint_proposal_job.d.ts +36 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +361 -192
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +21 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +97 -15
- package/dest/sequencer/sequencer.d.ts +40 -17
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +152 -95
- package/dest/sequencer/timetable.d.ts +7 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +21 -12
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +76 -30
- package/src/config.ts +56 -27
- package/src/global_variable_builder/global_builder.ts +38 -27
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +153 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -9
- package/src/publisher/sequencer-publisher.ts +503 -168
- package/src/sequencer/README.md +81 -12
- package/src/sequencer/checkpoint_proposal_job.ts +471 -201
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +106 -18
- package/src/sequencer/sequencer.ts +216 -109
- package/src/sequencer/timetable.ts +26 -15
- 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,36 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
|
|
|
372
372
|
}
|
|
373
373
|
var _dec, _dec1, _dec2, _initProto;
|
|
374
374
|
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
375
|
-
import { MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
|
|
375
|
+
import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
|
|
376
376
|
import { L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
377
377
|
import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
|
|
378
378
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
379
379
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
380
380
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
381
381
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
382
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
382
383
|
import { pick } from '@aztec/foundation/collection';
|
|
384
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
383
385
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
384
386
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
385
387
|
import { createLogger } from '@aztec/foundation/log';
|
|
388
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
389
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
386
390
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
387
391
|
import { Timer } from '@aztec/foundation/timer';
|
|
388
392
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
389
393
|
import { encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
390
394
|
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
395
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
391
396
|
import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
392
|
-
import { encodeFunctionData, toHex } from 'viem';
|
|
397
|
+
import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
|
|
398
|
+
import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
393
399
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
394
400
|
export const Actions = [
|
|
395
401
|
'invalidate-by-invalid-attestation',
|
|
396
402
|
'invalidate-by-insufficient-attestations',
|
|
397
403
|
'propose',
|
|
398
404
|
'governance-signal',
|
|
399
|
-
'empire-slashing-signal',
|
|
400
|
-
'create-empire-payload',
|
|
401
|
-
'execute-empire-payload',
|
|
402
405
|
'vote-offenses',
|
|
403
406
|
'execute-slash'
|
|
404
407
|
];
|
|
@@ -429,15 +432,22 @@ export class SequencerPublisher {
|
|
|
429
432
|
interrupted;
|
|
430
433
|
metrics;
|
|
431
434
|
epochCache;
|
|
435
|
+
failedTxStore;
|
|
432
436
|
governanceLog;
|
|
433
437
|
slashingLog;
|
|
434
438
|
lastActions;
|
|
435
439
|
isPayloadEmptyCache;
|
|
440
|
+
payloadProposedCache;
|
|
436
441
|
log;
|
|
437
442
|
ethereumSlotDuration;
|
|
443
|
+
aztecSlotDuration;
|
|
444
|
+
/** Date provider for wall-clock time. */ dateProvider;
|
|
438
445
|
blobClient;
|
|
439
446
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
447
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
|
|
440
448
|
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
449
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
|
|
450
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */ interruptibleSleep;
|
|
441
451
|
// A CALL to a cold address is 2700 gas
|
|
442
452
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
443
453
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -446,7 +456,6 @@ export class SequencerPublisher {
|
|
|
446
456
|
rollupContract;
|
|
447
457
|
govProposerContract;
|
|
448
458
|
slashingProposerContract;
|
|
449
|
-
slashFactoryContract;
|
|
450
459
|
tracer;
|
|
451
460
|
requests;
|
|
452
461
|
constructor(config, deps){
|
|
@@ -456,16 +465,22 @@ export class SequencerPublisher {
|
|
|
456
465
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
457
466
|
this.lastActions = {};
|
|
458
467
|
this.isPayloadEmptyCache = new Map();
|
|
468
|
+
this.payloadProposedCache = new Set();
|
|
469
|
+
this.interruptibleSleep = new InterruptibleSleep();
|
|
459
470
|
this.requests = [];
|
|
460
471
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
461
472
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
473
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
474
|
+
this.dateProvider = deps.dateProvider;
|
|
462
475
|
this.epochCache = deps.epochCache;
|
|
463
476
|
this.lastActions = deps.lastActions;
|
|
464
477
|
this.blobClient = deps.blobClient;
|
|
478
|
+
this.dateProvider = deps.dateProvider;
|
|
465
479
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
466
480
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
467
481
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
468
482
|
this.l1TxUtils = deps.l1TxUtils;
|
|
483
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
469
484
|
this.rollupContract = deps.rollupContract;
|
|
470
485
|
this.govProposerContract = deps.governanceProposerContract;
|
|
471
486
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
@@ -474,15 +489,40 @@ export class SequencerPublisher {
|
|
|
474
489
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
475
490
|
this.slashingProposerContract = newSlashingProposer;
|
|
476
491
|
});
|
|
477
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
478
492
|
// Initialize L1 fee analyzer for fisherman mode
|
|
479
493
|
if (config.fishermanMode) {
|
|
480
494
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
481
495
|
}
|
|
496
|
+
// Initialize fee asset price oracle
|
|
497
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
|
|
498
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
499
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
503
|
+
* Does nothing if no store is configured.
|
|
504
|
+
*/ backupFailedTx(failedTx) {
|
|
505
|
+
if (!this.failedTxStore) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const tx = {
|
|
509
|
+
...failedTx,
|
|
510
|
+
timestamp: Date.now()
|
|
511
|
+
};
|
|
512
|
+
// Fire and forget - don't block on backup
|
|
513
|
+
void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
|
|
514
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
515
|
+
});
|
|
482
516
|
}
|
|
483
517
|
getRollupContract() {
|
|
484
518
|
return this.rollupContract;
|
|
485
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Gets the fee asset price modifier from the oracle.
|
|
522
|
+
* Returns 0n if the oracle query fails.
|
|
523
|
+
*/ getFeeAssetPriceModifier() {
|
|
524
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
525
|
+
}
|
|
486
526
|
getSenderAddress() {
|
|
487
527
|
return this.l1TxUtils.getSenderAddress();
|
|
488
528
|
}
|
|
@@ -501,7 +541,7 @@ export class SequencerPublisher {
|
|
|
501
541
|
this.requests.push(request);
|
|
502
542
|
}
|
|
503
543
|
getCurrentL2Slot() {
|
|
504
|
-
return this.epochCache.
|
|
544
|
+
return this.epochCache.getSlotNow();
|
|
505
545
|
}
|
|
506
546
|
/**
|
|
507
547
|
* Clears all pending requests without sending them.
|
|
@@ -590,8 +630,8 @@ export class SequencerPublisher {
|
|
|
590
630
|
// @note - we can only have one blob config per bundle
|
|
591
631
|
// find requests with gas and blob configs
|
|
592
632
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
593
|
-
const gasConfigs =
|
|
594
|
-
const blobConfigs =
|
|
633
|
+
const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
|
|
634
|
+
const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
|
|
595
635
|
if (blobConfigs.length > 1) {
|
|
596
636
|
throw new Error('Multiple blob configs found');
|
|
597
637
|
}
|
|
@@ -618,12 +658,34 @@ export class SequencerPublisher {
|
|
|
618
658
|
// This ensures the committee gets precomputed correctly
|
|
619
659
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
620
660
|
try {
|
|
661
|
+
// Capture context for failed tx backup before sending
|
|
662
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
663
|
+
const multicallData = encodeFunctionData({
|
|
664
|
+
abi: multicall3Abi,
|
|
665
|
+
functionName: 'aggregate3',
|
|
666
|
+
args: [
|
|
667
|
+
validRequests.map((r)=>({
|
|
668
|
+
target: r.request.to,
|
|
669
|
+
callData: r.request.data,
|
|
670
|
+
allowFailure: true
|
|
671
|
+
}))
|
|
672
|
+
]
|
|
673
|
+
});
|
|
674
|
+
const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
|
|
675
|
+
const txContext = {
|
|
676
|
+
multicallData,
|
|
677
|
+
blobData: blobDataHex,
|
|
678
|
+
l1BlockNumber
|
|
679
|
+
};
|
|
621
680
|
this.log.debug('Forwarding transactions', {
|
|
622
681
|
validRequests: validRequests.map((request)=>request.action),
|
|
623
682
|
txConfig
|
|
624
683
|
});
|
|
625
|
-
const result = await
|
|
626
|
-
|
|
684
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
685
|
+
if (result === undefined) {
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
|
|
627
689
|
return {
|
|
628
690
|
result,
|
|
629
691
|
expiredActions,
|
|
@@ -643,17 +705,118 @@ export class SequencerPublisher {
|
|
|
643
705
|
}
|
|
644
706
|
}
|
|
645
707
|
}
|
|
646
|
-
|
|
708
|
+
/**
|
|
709
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
710
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
711
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
712
|
+
*/ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
|
|
713
|
+
const triedAddresses = [];
|
|
714
|
+
let currentPublisher = this.l1TxUtils;
|
|
715
|
+
while(true){
|
|
716
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
717
|
+
try {
|
|
718
|
+
const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
|
|
719
|
+
this.l1TxUtils = currentPublisher;
|
|
720
|
+
return result;
|
|
721
|
+
} catch (err) {
|
|
722
|
+
if (err instanceof TimeoutError) {
|
|
723
|
+
throw err;
|
|
724
|
+
}
|
|
725
|
+
const viemError = formatViemError(err);
|
|
726
|
+
if (!this.getNextPublisher) {
|
|
727
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
|
|
731
|
+
const nextPublisher = await this.getNextPublisher([
|
|
732
|
+
...triedAddresses
|
|
733
|
+
]);
|
|
734
|
+
if (!nextPublisher) {
|
|
735
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
currentPublisher = nextPublisher;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/*
|
|
743
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
744
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
745
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
746
|
+
*/ async sendRequestsAt(submitAfter) {
|
|
747
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
748
|
+
if (ms > 0) {
|
|
749
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, {
|
|
750
|
+
submitAfter
|
|
751
|
+
});
|
|
752
|
+
await this.interruptibleSleep.sleep(ms);
|
|
753
|
+
}
|
|
754
|
+
if (this.interrupted) {
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
757
|
+
// Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
|
|
758
|
+
const validRequests = [];
|
|
759
|
+
for (const request of this.requests){
|
|
760
|
+
if (!request.preCheck) {
|
|
761
|
+
validRequests.push(request);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
await request.preCheck();
|
|
766
|
+
validRequests.push(request);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
this.requests = validRequests;
|
|
772
|
+
if (this.requests.length === 0) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
return this.sendRequests();
|
|
776
|
+
}
|
|
777
|
+
callbackBundledTransactions(requests, result, txContext) {
|
|
647
778
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
648
779
|
if (result instanceof FormattedViemError) {
|
|
649
780
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
781
|
+
this.backupFailedTx({
|
|
782
|
+
id: keccak256(txContext.multicallData),
|
|
783
|
+
failureType: 'send-error',
|
|
784
|
+
request: {
|
|
785
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
786
|
+
data: txContext.multicallData
|
|
787
|
+
},
|
|
788
|
+
blobData: txContext.blobData,
|
|
789
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
790
|
+
error: {
|
|
791
|
+
message: result.message,
|
|
792
|
+
name: result.name
|
|
793
|
+
},
|
|
794
|
+
context: {
|
|
795
|
+
actions: requests.map((r)=>r.action),
|
|
796
|
+
requests: requests.map((r)=>({
|
|
797
|
+
action: r.action,
|
|
798
|
+
to: r.request.to,
|
|
799
|
+
data: r.request.data
|
|
800
|
+
})),
|
|
801
|
+
sender: this.getSenderAddress().toString()
|
|
802
|
+
}
|
|
803
|
+
});
|
|
650
804
|
return {
|
|
651
805
|
failedActions: requests.map((r)=>r.action)
|
|
652
806
|
};
|
|
653
807
|
} else {
|
|
654
808
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
655
809
|
result,
|
|
656
|
-
requests
|
|
810
|
+
requests: requests.map((r)=>({
|
|
811
|
+
...r,
|
|
812
|
+
// Avoid logging large blob data
|
|
813
|
+
blobConfig: r.blobConfig ? {
|
|
814
|
+
...r.blobConfig,
|
|
815
|
+
blobs: r.blobConfig.blobs.map((b)=>({
|
|
816
|
+
size: trimmedBytesLength(b)
|
|
817
|
+
}))
|
|
818
|
+
} : undefined
|
|
819
|
+
}))
|
|
657
820
|
});
|
|
658
821
|
const successfulActions = [];
|
|
659
822
|
const failedActions = [];
|
|
@@ -664,6 +827,37 @@ export class SequencerPublisher {
|
|
|
664
827
|
failedActions.push(request.action);
|
|
665
828
|
}
|
|
666
829
|
}
|
|
830
|
+
// Single backup for the whole reverted tx
|
|
831
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
832
|
+
this.backupFailedTx({
|
|
833
|
+
id: result.receipt.transactionHash,
|
|
834
|
+
failureType: 'revert',
|
|
835
|
+
request: {
|
|
836
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
837
|
+
data: txContext.multicallData
|
|
838
|
+
},
|
|
839
|
+
blobData: txContext.blobData,
|
|
840
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
841
|
+
receipt: {
|
|
842
|
+
transactionHash: result.receipt.transactionHash,
|
|
843
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
844
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
845
|
+
status: 'reverted'
|
|
846
|
+
},
|
|
847
|
+
error: {
|
|
848
|
+
message: result.errorMsg ?? 'Transaction reverted'
|
|
849
|
+
},
|
|
850
|
+
context: {
|
|
851
|
+
actions: failedActions,
|
|
852
|
+
requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
|
|
853
|
+
action: r.action,
|
|
854
|
+
to: r.request.to,
|
|
855
|
+
data: r.request.data
|
|
856
|
+
})),
|
|
857
|
+
sender: this.getSenderAddress().toString()
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
667
861
|
return {
|
|
668
862
|
successfulActions,
|
|
669
863
|
failedActions
|
|
@@ -671,18 +865,22 @@ export class SequencerPublisher {
|
|
|
671
865
|
}
|
|
672
866
|
}
|
|
673
867
|
/**
|
|
674
|
-
* @notice Will call `
|
|
868
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
675
869
|
* @param tipArchive - The archive to check
|
|
676
870
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
677
|
-
*/
|
|
871
|
+
*/ canProposeAt(tipArchive, msgSender, opts = {}) {
|
|
678
872
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
679
873
|
const ignoredErrors = [
|
|
680
874
|
'SlotAlreadyInChain',
|
|
681
875
|
'InvalidProposer',
|
|
682
876
|
'InvalidArchive'
|
|
683
877
|
];
|
|
684
|
-
|
|
685
|
-
|
|
878
|
+
const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
|
|
879
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
880
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
881
|
+
return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
882
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
883
|
+
forceArchive: opts.forceArchive
|
|
686
884
|
}).catch((err)=>{
|
|
687
885
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
688
886
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
@@ -713,7 +911,7 @@ export class SequencerPublisher {
|
|
|
713
911
|
header.blobsHash.toString(),
|
|
714
912
|
flags
|
|
715
913
|
];
|
|
716
|
-
const ts =
|
|
914
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
717
915
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
|
|
718
916
|
let balance = 0n;
|
|
719
917
|
if (this.config.fishermanMode) {
|
|
@@ -736,7 +934,7 @@ export class SequencerPublisher {
|
|
|
736
934
|
}),
|
|
737
935
|
from: MULTI_CALL_3_ADDRESS
|
|
738
936
|
}, {
|
|
739
|
-
time: ts
|
|
937
|
+
time: ts
|
|
740
938
|
}, stateOverrides);
|
|
741
939
|
this.log.debug(`Simulated validateHeader`);
|
|
742
940
|
}
|
|
@@ -766,6 +964,7 @@ export class SequencerPublisher {
|
|
|
766
964
|
...logData,
|
|
767
965
|
request
|
|
768
966
|
});
|
|
967
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
769
968
|
try {
|
|
770
969
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
771
970
|
request.abi ?? [],
|
|
@@ -781,6 +980,7 @@ export class SequencerPublisher {
|
|
|
781
980
|
gasUsed,
|
|
782
981
|
checkpointNumber,
|
|
783
982
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
983
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
784
984
|
reason
|
|
785
985
|
};
|
|
786
986
|
} catch (err) {
|
|
@@ -793,8 +993,8 @@ export class SequencerPublisher {
|
|
|
793
993
|
request,
|
|
794
994
|
error: viemError.message
|
|
795
995
|
});
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
996
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
997
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
798
998
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
|
|
799
999
|
...logData
|
|
800
1000
|
});
|
|
@@ -808,6 +1008,27 @@ export class SequencerPublisher {
|
|
|
808
1008
|
}
|
|
809
1009
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
810
1010
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
1011
|
+
this.backupFailedTx({
|
|
1012
|
+
id: keccak256(request.data),
|
|
1013
|
+
failureType: 'simulation',
|
|
1014
|
+
request: {
|
|
1015
|
+
to: request.to,
|
|
1016
|
+
data: request.data,
|
|
1017
|
+
value: request.value?.toString()
|
|
1018
|
+
},
|
|
1019
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1020
|
+
error: {
|
|
1021
|
+
message: viemError.message,
|
|
1022
|
+
name: viemError.name
|
|
1023
|
+
},
|
|
1024
|
+
context: {
|
|
1025
|
+
actions: [
|
|
1026
|
+
`invalidate-${reason}`
|
|
1027
|
+
],
|
|
1028
|
+
checkpointNumber,
|
|
1029
|
+
sender: this.getSenderAddress().toString()
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
811
1032
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
812
1033
|
cause: viemError
|
|
813
1034
|
});
|
|
@@ -834,30 +1055,15 @@ export class SequencerPublisher {
|
|
|
834
1055
|
}
|
|
835
1056
|
}
|
|
836
1057
|
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
|
|
837
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
838
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
839
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
840
|
-
// so that the committee is recalculated correctly
|
|
841
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
842
|
-
// if (ignoreSignatures) {
|
|
843
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
844
|
-
// if (!committee) {
|
|
845
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
846
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
847
|
-
// }
|
|
848
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
849
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
850
|
-
// );
|
|
851
|
-
// }
|
|
852
1058
|
const blobFields = checkpoint.toBlobFields();
|
|
853
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1059
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
854
1060
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
855
1061
|
const args = [
|
|
856
1062
|
{
|
|
857
1063
|
header: checkpoint.header.toViem(),
|
|
858
1064
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
859
1065
|
oracleInput: {
|
|
860
|
-
feeAssetPriceModifier:
|
|
1066
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
861
1067
|
}
|
|
862
1068
|
},
|
|
863
1069
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -865,10 +1071,9 @@ export class SequencerPublisher {
|
|
|
865
1071
|
attestationsAndSignersSignature.toViemSignature(),
|
|
866
1072
|
blobInput
|
|
867
1073
|
];
|
|
868
|
-
await this.simulateProposeTx(args,
|
|
869
|
-
return ts;
|
|
1074
|
+
await this.simulateProposeTx(args, options);
|
|
870
1075
|
}
|
|
871
|
-
async enqueueCastSignalHelper(slotNumber,
|
|
1076
|
+
async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
|
|
872
1077
|
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
873
1078
|
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
874
1079
|
return false;
|
|
@@ -892,6 +1097,28 @@ export class SequencerPublisher {
|
|
|
892
1097
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
893
1098
|
return false;
|
|
894
1099
|
}
|
|
1100
|
+
// Check if payload was already submitted to governance
|
|
1101
|
+
const cacheKey = payload.toString();
|
|
1102
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
1103
|
+
try {
|
|
1104
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
1105
|
+
const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
|
|
1106
|
+
0,
|
|
1107
|
+
1,
|
|
1108
|
+
2
|
|
1109
|
+
]), this.log, true);
|
|
1110
|
+
if (proposed) {
|
|
1111
|
+
this.payloadProposedCache.add(cacheKey);
|
|
1112
|
+
}
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
1119
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
895
1122
|
const cachedLastVote = this.lastActions[signalType];
|
|
896
1123
|
this.lastActions[signalType] = slotNumber;
|
|
897
1124
|
const action = signalType;
|
|
@@ -902,6 +1129,8 @@ export class SequencerPublisher {
|
|
|
902
1129
|
signer: this.l1TxUtils.client.account?.address,
|
|
903
1130
|
lastValidL2Slot: slotNumber
|
|
904
1131
|
});
|
|
1132
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1133
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
905
1134
|
try {
|
|
906
1135
|
await this.l1TxUtils.simulate(request, {
|
|
907
1136
|
time: timestamp
|
|
@@ -913,7 +1142,32 @@ export class SequencerPublisher {
|
|
|
913
1142
|
request
|
|
914
1143
|
});
|
|
915
1144
|
} catch (err) {
|
|
916
|
-
|
|
1145
|
+
const viemError = formatViemError(err);
|
|
1146
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
1147
|
+
simulationTimestamp: timestamp,
|
|
1148
|
+
l1BlockNumber
|
|
1149
|
+
});
|
|
1150
|
+
this.backupFailedTx({
|
|
1151
|
+
id: keccak256(request.data),
|
|
1152
|
+
failureType: 'simulation',
|
|
1153
|
+
request: {
|
|
1154
|
+
to: request.to,
|
|
1155
|
+
data: request.data,
|
|
1156
|
+
value: request.value?.toString()
|
|
1157
|
+
},
|
|
1158
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1159
|
+
error: {
|
|
1160
|
+
message: viemError.message,
|
|
1161
|
+
name: viemError.name
|
|
1162
|
+
},
|
|
1163
|
+
context: {
|
|
1164
|
+
actions: [
|
|
1165
|
+
action
|
|
1166
|
+
],
|
|
1167
|
+
slot: slotNumber,
|
|
1168
|
+
sender: this.getSenderAddress().toString()
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
917
1171
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
918
1172
|
}
|
|
919
1173
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -957,55 +1211,17 @@ export class SequencerPublisher {
|
|
|
957
1211
|
/**
|
|
958
1212
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
959
1213
|
* @param slotNumber - The slot number to cast a signal for.
|
|
960
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
961
1214
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
962
|
-
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber,
|
|
963
|
-
return this.enqueueCastSignalHelper(slotNumber,
|
|
1215
|
+
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
|
|
1216
|
+
return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
964
1217
|
}
|
|
965
|
-
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber,
|
|
1218
|
+
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
|
|
966
1219
|
if (actions.length === 0) {
|
|
967
1220
|
this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
|
|
968
1221
|
return false;
|
|
969
1222
|
}
|
|
970
1223
|
for (const action of actions){
|
|
971
1224
|
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
1225
|
case 'vote-offenses':
|
|
1010
1226
|
{
|
|
1011
1227
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
@@ -1014,14 +1230,13 @@ export class SequencerPublisher {
|
|
|
1014
1230
|
votesCount: action.votes.length,
|
|
1015
1231
|
signerAddress
|
|
1016
1232
|
});
|
|
1017
|
-
if (this.slashingProposerContract
|
|
1018
|
-
this.log.error('
|
|
1233
|
+
if (!this.slashingProposerContract) {
|
|
1234
|
+
this.log.error('No slashing proposer contract available');
|
|
1019
1235
|
return false;
|
|
1020
1236
|
}
|
|
1021
|
-
const tallySlashingProposer = this.slashingProposerContract;
|
|
1022
1237
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1023
|
-
const request = await
|
|
1024
|
-
await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!
|
|
1238
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1239
|
+
await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!this.slashingProposerContract.tryExtractVoteCastEvent(receipt.logs), slotNumber);
|
|
1025
1240
|
break;
|
|
1026
1241
|
}
|
|
1027
1242
|
case 'execute-slash':
|
|
@@ -1031,13 +1246,12 @@ export class SequencerPublisher {
|
|
|
1031
1246
|
round: action.round,
|
|
1032
1247
|
signerAddress
|
|
1033
1248
|
});
|
|
1034
|
-
if (this.slashingProposerContract
|
|
1035
|
-
this.log.error('
|
|
1249
|
+
if (!this.slashingProposerContract) {
|
|
1250
|
+
this.log.error('No slashing proposer contract available');
|
|
1036
1251
|
return false;
|
|
1037
1252
|
}
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber, timestamp);
|
|
1253
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(action.round, action.committees);
|
|
1254
|
+
await this.simulateAndEnqueueRequest('execute-slash', executeRequest, (receipt)=>!!this.slashingProposerContract.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
|
|
1041
1255
|
break;
|
|
1042
1256
|
}
|
|
1043
1257
|
default:
|
|
@@ -1052,22 +1266,22 @@ export class SequencerPublisher {
|
|
|
1052
1266
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1053
1267
|
const checkpointHeader = checkpoint.header;
|
|
1054
1268
|
const blobFields = checkpoint.toBlobFields();
|
|
1055
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1269
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1056
1270
|
const proposeTxArgs = {
|
|
1057
1271
|
header: checkpointHeader,
|
|
1058
1272
|
archive: checkpoint.archive.root.toBuffer(),
|
|
1059
1273
|
blobs,
|
|
1060
1274
|
attestationsAndSigners,
|
|
1061
|
-
attestationsAndSignersSignature
|
|
1275
|
+
attestationsAndSignersSignature,
|
|
1276
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1062
1277
|
};
|
|
1063
|
-
let ts;
|
|
1064
1278
|
try {
|
|
1065
1279
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
1066
1280
|
// This means that we can avoid the simulation issues in later checks.
|
|
1067
1281
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1068
1282
|
// make time consistency checks break.
|
|
1069
1283
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1070
|
-
|
|
1284
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
|
|
1071
1285
|
} catch (err) {
|
|
1072
1286
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1073
1287
|
...checkpoint.getStats(),
|
|
@@ -1076,11 +1290,23 @@ export class SequencerPublisher {
|
|
|
1076
1290
|
});
|
|
1077
1291
|
throw err;
|
|
1078
1292
|
}
|
|
1293
|
+
// Build a pre-check callback that re-validates the checkpoint before L1 submission.
|
|
1294
|
+
// During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
|
|
1295
|
+
let preCheck = undefined;
|
|
1296
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
1297
|
+
preCheck = async ()=>{
|
|
1298
|
+
this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
|
|
1299
|
+
await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, {
|
|
1300
|
+
// Forcing pending checkpoint number is included its required if an invalidation request is included
|
|
1301
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
|
|
1302
|
+
});
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1079
1305
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1080
1306
|
...checkpoint.toCheckpointInfo(),
|
|
1081
1307
|
...opts
|
|
1082
1308
|
});
|
|
1083
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts,
|
|
1309
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, preCheck);
|
|
1084
1310
|
}
|
|
1085
1311
|
enqueueInvalidateCheckpoint(request, opts = {}) {
|
|
1086
1312
|
if (!request) {
|
|
@@ -1121,7 +1347,8 @@ export class SequencerPublisher {
|
|
|
1121
1347
|
}
|
|
1122
1348
|
});
|
|
1123
1349
|
}
|
|
1124
|
-
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber
|
|
1350
|
+
async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
|
|
1351
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1125
1352
|
const logData = {
|
|
1126
1353
|
slotNumber,
|
|
1127
1354
|
timestamp,
|
|
@@ -1134,6 +1361,7 @@ export class SequencerPublisher {
|
|
|
1134
1361
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1135
1362
|
this.lastActions[action] = slotNumber;
|
|
1136
1363
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1364
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1137
1365
|
let gasUsed;
|
|
1138
1366
|
const simulateAbi = mergeAbis([
|
|
1139
1367
|
request.abi ?? [],
|
|
@@ -1142,7 +1370,7 @@ export class SequencerPublisher {
|
|
|
1142
1370
|
try {
|
|
1143
1371
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
1144
1372
|
time: timestamp
|
|
1145
|
-
}, [], simulateAbi));
|
|
1373
|
+
}, [], simulateAbi));
|
|
1146
1374
|
this.log.verbose(`Simulation for ${action} succeeded`, {
|
|
1147
1375
|
...logData,
|
|
1148
1376
|
request,
|
|
@@ -1151,6 +1379,27 @@ export class SequencerPublisher {
|
|
|
1151
1379
|
} catch (err) {
|
|
1152
1380
|
const viemError = formatViemError(err, simulateAbi);
|
|
1153
1381
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1382
|
+
this.backupFailedTx({
|
|
1383
|
+
id: keccak256(request.data),
|
|
1384
|
+
failureType: 'simulation',
|
|
1385
|
+
request: {
|
|
1386
|
+
to: request.to,
|
|
1387
|
+
data: request.data,
|
|
1388
|
+
value: request.value?.toString()
|
|
1389
|
+
},
|
|
1390
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1391
|
+
error: {
|
|
1392
|
+
message: viemError.message,
|
|
1393
|
+
name: viemError.name
|
|
1394
|
+
},
|
|
1395
|
+
context: {
|
|
1396
|
+
actions: [
|
|
1397
|
+
action
|
|
1398
|
+
],
|
|
1399
|
+
slot: slotNumber,
|
|
1400
|
+
sender: this.getSenderAddress().toString()
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1154
1403
|
return false;
|
|
1155
1404
|
}
|
|
1156
1405
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
@@ -1196,13 +1445,14 @@ export class SequencerPublisher {
|
|
|
1196
1445
|
* A call to `restart` is required before you can continue publishing.
|
|
1197
1446
|
*/ interrupt() {
|
|
1198
1447
|
this.interrupted = true;
|
|
1448
|
+
this.interruptibleSleep.interrupt();
|
|
1199
1449
|
this.l1TxUtils.interrupt();
|
|
1200
1450
|
}
|
|
1201
1451
|
/** Restarts the publisher after calling `interrupt`. */ restart() {
|
|
1202
1452
|
this.interrupted = false;
|
|
1203
1453
|
this.l1TxUtils.restart();
|
|
1204
1454
|
}
|
|
1205
|
-
async prepareProposeTx(encodedData,
|
|
1455
|
+
async prepareProposeTx(encodedData, options) {
|
|
1206
1456
|
const kzg = Blob.getViemKzgInstance();
|
|
1207
1457
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1208
1458
|
this.log.debug('Validating blob input', {
|
|
@@ -1229,10 +1479,38 @@ export class SequencerPublisher {
|
|
|
1229
1479
|
}, {}, {
|
|
1230
1480
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1231
1481
|
kzg
|
|
1232
|
-
}).catch((err)=>{
|
|
1233
|
-
const
|
|
1234
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1235
|
-
metaMessages
|
|
1482
|
+
}).catch(async (err)=>{
|
|
1483
|
+
const viemError = formatViemError(err);
|
|
1484
|
+
this.log.error(`Failed to validate blobs`, viemError.message, {
|
|
1485
|
+
metaMessages: viemError.metaMessages
|
|
1486
|
+
});
|
|
1487
|
+
const validateBlobsData = encodeFunctionData({
|
|
1488
|
+
abi: RollupAbi,
|
|
1489
|
+
functionName: 'validateBlobs',
|
|
1490
|
+
args: [
|
|
1491
|
+
blobInput
|
|
1492
|
+
]
|
|
1493
|
+
});
|
|
1494
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1495
|
+
this.backupFailedTx({
|
|
1496
|
+
id: keccak256(validateBlobsData),
|
|
1497
|
+
failureType: 'simulation',
|
|
1498
|
+
request: {
|
|
1499
|
+
to: this.rollupContract.address,
|
|
1500
|
+
data: validateBlobsData
|
|
1501
|
+
},
|
|
1502
|
+
blobData: encodedData.blobs.map((b)=>toHex(b.data)),
|
|
1503
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1504
|
+
error: {
|
|
1505
|
+
message: viemError.message,
|
|
1506
|
+
name: viemError.name
|
|
1507
|
+
},
|
|
1508
|
+
context: {
|
|
1509
|
+
actions: [
|
|
1510
|
+
'validate-blobs'
|
|
1511
|
+
],
|
|
1512
|
+
sender: this.getSenderAddress().toString()
|
|
1513
|
+
}
|
|
1236
1514
|
});
|
|
1237
1515
|
throw new Error('Failed to validate blobs');
|
|
1238
1516
|
});
|
|
@@ -1243,8 +1521,7 @@ export class SequencerPublisher {
|
|
|
1243
1521
|
header: encodedData.header.toViem(),
|
|
1244
1522
|
archive: toHex(encodedData.archive),
|
|
1245
1523
|
oracleInput: {
|
|
1246
|
-
|
|
1247
|
-
feeAssetPriceModifier: 0n
|
|
1524
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier
|
|
1248
1525
|
}
|
|
1249
1526
|
},
|
|
1250
1527
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1252,7 +1529,7 @@ export class SequencerPublisher {
|
|
|
1252
1529
|
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
1253
1530
|
blobInput
|
|
1254
1531
|
];
|
|
1255
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1532
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
|
|
1256
1533
|
return {
|
|
1257
1534
|
args,
|
|
1258
1535
|
blobEvaluationGas,
|
|
@@ -1263,16 +1540,17 @@ export class SequencerPublisher {
|
|
|
1263
1540
|
/**
|
|
1264
1541
|
* Simulates the propose tx with eth_simulateV1
|
|
1265
1542
|
* @param args - The propose tx args
|
|
1266
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1267
1543
|
* @returns The simulation result
|
|
1268
|
-
*/ async simulateProposeTx(args,
|
|
1544
|
+
*/ async simulateProposeTx(args, options) {
|
|
1269
1545
|
const rollupData = encodeFunctionData({
|
|
1270
1546
|
abi: RollupAbi,
|
|
1271
1547
|
functionName: 'propose',
|
|
1272
1548
|
args
|
|
1273
1549
|
});
|
|
1274
|
-
// override the
|
|
1550
|
+
// override the proposed checkpoint number if requested
|
|
1275
1551
|
const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1552
|
+
// override the fee header for a specific checkpoint number if requested (used when pipelining)
|
|
1553
|
+
const forceProposedFeeHeaderStateDiff = (options.forceProposedFeeHeader !== undefined ? await this.rollupContract.makeFeeHeaderOverride(options.forceProposedFeeHeader.checkpointNumber, options.forceProposedFeeHeader.feeHeader) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
1276
1554
|
const stateOverrides = [
|
|
1277
1555
|
{
|
|
1278
1556
|
address: this.rollupContract.address,
|
|
@@ -1282,7 +1560,8 @@ export class SequencerPublisher {
|
|
|
1282
1560
|
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
1283
1561
|
value: toPaddedHex(0n, true)
|
|
1284
1562
|
},
|
|
1285
|
-
...forcePendingCheckpointNumberStateDiff
|
|
1563
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1564
|
+
...forceProposedFeeHeaderStateDiff
|
|
1286
1565
|
]
|
|
1287
1566
|
}
|
|
1288
1567
|
];
|
|
@@ -1293,6 +1572,8 @@ export class SequencerPublisher {
|
|
|
1293
1572
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1294
1573
|
});
|
|
1295
1574
|
}
|
|
1575
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1576
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1296
1577
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1297
1578
|
to: this.rollupContract.address,
|
|
1298
1579
|
data: rollupData,
|
|
@@ -1301,8 +1582,7 @@ export class SequencerPublisher {
|
|
|
1301
1582
|
from: this.proposerAddressForSimulation.toString()
|
|
1302
1583
|
}
|
|
1303
1584
|
}, {
|
|
1304
|
-
|
|
1305
|
-
time: timestamp + 1n,
|
|
1585
|
+
time: simTs,
|
|
1306
1586
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1307
1587
|
gasLimit: MAX_L1_TX_LIMIT * 2n
|
|
1308
1588
|
}, stateOverrides, RollupAbi, {
|
|
@@ -1319,7 +1599,29 @@ export class SequencerPublisher {
|
|
|
1319
1599
|
logs: []
|
|
1320
1600
|
};
|
|
1321
1601
|
}
|
|
1322
|
-
this.log.error(`Failed to simulate propose tx`, viemError
|
|
1602
|
+
this.log.error(`Failed to simulate propose tx`, viemError, {
|
|
1603
|
+
simulationTimestamp: simTs
|
|
1604
|
+
});
|
|
1605
|
+
this.backupFailedTx({
|
|
1606
|
+
id: keccak256(rollupData),
|
|
1607
|
+
failureType: 'simulation',
|
|
1608
|
+
request: {
|
|
1609
|
+
to: this.rollupContract.address,
|
|
1610
|
+
data: rollupData
|
|
1611
|
+
},
|
|
1612
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1613
|
+
error: {
|
|
1614
|
+
message: viemError.message,
|
|
1615
|
+
name: viemError.name
|
|
1616
|
+
},
|
|
1617
|
+
context: {
|
|
1618
|
+
actions: [
|
|
1619
|
+
'propose'
|
|
1620
|
+
],
|
|
1621
|
+
slot: Number(args[0].header.slotNumber),
|
|
1622
|
+
sender: this.getSenderAddress().toString()
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1323
1625
|
throw err;
|
|
1324
1626
|
});
|
|
1325
1627
|
return {
|
|
@@ -1327,11 +1629,11 @@ export class SequencerPublisher {
|
|
|
1327
1629
|
simulationResult
|
|
1328
1630
|
};
|
|
1329
1631
|
}
|
|
1330
|
-
async addProposeTx(checkpoint, encodedData, opts = {},
|
|
1632
|
+
async addProposeTx(checkpoint, encodedData, opts = {}, preCheck) {
|
|
1331
1633
|
const slot = checkpoint.header.slotNumber;
|
|
1332
1634
|
const timer = new Timer();
|
|
1333
1635
|
const kzg = Blob.getViemKzgInstance();
|
|
1334
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData,
|
|
1636
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
|
|
1335
1637
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1336
1638
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
1337
1639
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
@@ -1350,6 +1652,7 @@ export class SequencerPublisher {
|
|
|
1350
1652
|
...opts,
|
|
1351
1653
|
gasLimit
|
|
1352
1654
|
},
|
|
1655
|
+
preCheck,
|
|
1353
1656
|
blobConfig: {
|
|
1354
1657
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1355
1658
|
kzg
|
|
@@ -1396,4 +1699,13 @@ export class SequencerPublisher {
|
|
|
1396
1699
|
}
|
|
1397
1700
|
});
|
|
1398
1701
|
}
|
|
1702
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1703
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
|
|
1704
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1705
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1706
|
+
}
|
|
1707
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
|
|
1708
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1709
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1710
|
+
}
|
|
1399
1711
|
}
|