@aztec/sequencer-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ee6fcc6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +15 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +60 -26
- package/dest/config.d.ts +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +47 -28
- package/dest/global_variable_builder/global_builder.d.ts +14 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +22 -21
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher.d.ts +32 -9
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +343 -39
- package/dest/sequencer/checkpoint_proposal_job.d.ts +15 -7
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +240 -139
- 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 +28 -15
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +93 -84
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +76 -23
- package/src/config.ts +65 -38
- package/src/global_variable_builder/global_builder.ts +23 -24
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +153 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -6
- package/src/publisher/sequencer-publisher.ts +349 -53
- package/src/sequencer/checkpoint_proposal_job.ts +327 -150
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +106 -18
- package/src/sequencer/sequencer.ts +127 -96
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +63 -49
- package/src/test/utils.ts +5 -2
|
@@ -372,24 +372,29 @@ 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';
|
|
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 { 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',
|
|
@@ -429,15 +434,21 @@ export class SequencerPublisher {
|
|
|
429
434
|
interrupted;
|
|
430
435
|
metrics;
|
|
431
436
|
epochCache;
|
|
437
|
+
failedTxStore;
|
|
432
438
|
governanceLog;
|
|
433
439
|
slashingLog;
|
|
434
440
|
lastActions;
|
|
435
441
|
isPayloadEmptyCache;
|
|
442
|
+
payloadProposedCache;
|
|
436
443
|
log;
|
|
437
444
|
ethereumSlotDuration;
|
|
445
|
+
aztecSlotDuration;
|
|
446
|
+
dateProvider;
|
|
438
447
|
blobClient;
|
|
439
448
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
449
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
|
|
440
450
|
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
451
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
|
|
441
452
|
// A CALL to a cold address is 2700 gas
|
|
442
453
|
static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
443
454
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
@@ -456,9 +467,12 @@ export class SequencerPublisher {
|
|
|
456
467
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
457
468
|
this.lastActions = {};
|
|
458
469
|
this.isPayloadEmptyCache = new Map();
|
|
470
|
+
this.payloadProposedCache = new Set();
|
|
459
471
|
this.requests = [];
|
|
460
472
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
461
473
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
474
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
475
|
+
this.dateProvider = deps.dateProvider;
|
|
462
476
|
this.epochCache = deps.epochCache;
|
|
463
477
|
this.lastActions = deps.lastActions;
|
|
464
478
|
this.blobClient = deps.blobClient;
|
|
@@ -466,6 +480,7 @@ export class SequencerPublisher {
|
|
|
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;
|
|
@@ -479,10 +494,36 @@ export class SequencerPublisher {
|
|
|
479
494
|
if (config.fishermanMode) {
|
|
480
495
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
481
496
|
}
|
|
497
|
+
// Initialize fee asset price oracle
|
|
498
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
|
|
499
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
500
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
504
|
+
* Does nothing if no store is configured.
|
|
505
|
+
*/ backupFailedTx(failedTx) {
|
|
506
|
+
if (!this.failedTxStore) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const tx = {
|
|
510
|
+
...failedTx,
|
|
511
|
+
timestamp: Date.now()
|
|
512
|
+
};
|
|
513
|
+
// Fire and forget - don't block on backup
|
|
514
|
+
void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
|
|
515
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
516
|
+
});
|
|
482
517
|
}
|
|
483
518
|
getRollupContract() {
|
|
484
519
|
return this.rollupContract;
|
|
485
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Gets the fee asset price modifier from the oracle.
|
|
523
|
+
* Returns 0n if the oracle query fails.
|
|
524
|
+
*/ getFeeAssetPriceModifier() {
|
|
525
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
526
|
+
}
|
|
486
527
|
getSenderAddress() {
|
|
487
528
|
return this.l1TxUtils.getSenderAddress();
|
|
488
529
|
}
|
|
@@ -501,7 +542,7 @@ export class SequencerPublisher {
|
|
|
501
542
|
this.requests.push(request);
|
|
502
543
|
}
|
|
503
544
|
getCurrentL2Slot() {
|
|
504
|
-
return this.epochCache.
|
|
545
|
+
return this.epochCache.getSlotNow();
|
|
505
546
|
}
|
|
506
547
|
/**
|
|
507
548
|
* Clears all pending requests without sending them.
|
|
@@ -590,8 +631,8 @@ export class SequencerPublisher {
|
|
|
590
631
|
// @note - we can only have one blob config per bundle
|
|
591
632
|
// find requests with gas and blob configs
|
|
592
633
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
593
|
-
const gasConfigs =
|
|
594
|
-
const blobConfigs =
|
|
634
|
+
const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
|
|
635
|
+
const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
|
|
595
636
|
if (blobConfigs.length > 1) {
|
|
596
637
|
throw new Error('Multiple blob configs found');
|
|
597
638
|
}
|
|
@@ -618,12 +659,34 @@ export class SequencerPublisher {
|
|
|
618
659
|
// This ensures the committee gets precomputed correctly
|
|
619
660
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
620
661
|
try {
|
|
662
|
+
// Capture context for failed tx backup before sending
|
|
663
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
664
|
+
const multicallData = encodeFunctionData({
|
|
665
|
+
abi: multicall3Abi,
|
|
666
|
+
functionName: 'aggregate3',
|
|
667
|
+
args: [
|
|
668
|
+
validRequests.map((r)=>({
|
|
669
|
+
target: r.request.to,
|
|
670
|
+
callData: r.request.data,
|
|
671
|
+
allowFailure: true
|
|
672
|
+
}))
|
|
673
|
+
]
|
|
674
|
+
});
|
|
675
|
+
const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
|
|
676
|
+
const txContext = {
|
|
677
|
+
multicallData,
|
|
678
|
+
blobData: blobDataHex,
|
|
679
|
+
l1BlockNumber
|
|
680
|
+
};
|
|
621
681
|
this.log.debug('Forwarding transactions', {
|
|
622
682
|
validRequests: validRequests.map((request)=>request.action),
|
|
623
683
|
txConfig
|
|
624
684
|
});
|
|
625
|
-
const result = await
|
|
626
|
-
|
|
685
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
686
|
+
if (result === undefined) {
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
|
|
627
690
|
return {
|
|
628
691
|
result,
|
|
629
692
|
expiredActions,
|
|
@@ -643,17 +706,83 @@ export class SequencerPublisher {
|
|
|
643
706
|
}
|
|
644
707
|
}
|
|
645
708
|
}
|
|
646
|
-
|
|
709
|
+
/**
|
|
710
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
711
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
712
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
713
|
+
*/ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
|
|
714
|
+
const triedAddresses = [];
|
|
715
|
+
let currentPublisher = this.l1TxUtils;
|
|
716
|
+
while(true){
|
|
717
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
718
|
+
try {
|
|
719
|
+
const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
|
|
720
|
+
this.l1TxUtils = currentPublisher;
|
|
721
|
+
return result;
|
|
722
|
+
} catch (err) {
|
|
723
|
+
if (err instanceof TimeoutError) {
|
|
724
|
+
throw err;
|
|
725
|
+
}
|
|
726
|
+
const viemError = formatViemError(err);
|
|
727
|
+
if (!this.getNextPublisher) {
|
|
728
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
|
|
732
|
+
const nextPublisher = await this.getNextPublisher([
|
|
733
|
+
...triedAddresses
|
|
734
|
+
]);
|
|
735
|
+
if (!nextPublisher) {
|
|
736
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
currentPublisher = nextPublisher;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
callbackBundledTransactions(requests, result, txContext) {
|
|
647
744
|
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
648
745
|
if (result instanceof FormattedViemError) {
|
|
649
746
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
747
|
+
this.backupFailedTx({
|
|
748
|
+
id: keccak256(txContext.multicallData),
|
|
749
|
+
failureType: 'send-error',
|
|
750
|
+
request: {
|
|
751
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
752
|
+
data: txContext.multicallData
|
|
753
|
+
},
|
|
754
|
+
blobData: txContext.blobData,
|
|
755
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
756
|
+
error: {
|
|
757
|
+
message: result.message,
|
|
758
|
+
name: result.name
|
|
759
|
+
},
|
|
760
|
+
context: {
|
|
761
|
+
actions: requests.map((r)=>r.action),
|
|
762
|
+
requests: requests.map((r)=>({
|
|
763
|
+
action: r.action,
|
|
764
|
+
to: r.request.to,
|
|
765
|
+
data: r.request.data
|
|
766
|
+
})),
|
|
767
|
+
sender: this.getSenderAddress().toString()
|
|
768
|
+
}
|
|
769
|
+
});
|
|
650
770
|
return {
|
|
651
771
|
failedActions: requests.map((r)=>r.action)
|
|
652
772
|
};
|
|
653
773
|
} else {
|
|
654
774
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
655
775
|
result,
|
|
656
|
-
requests
|
|
776
|
+
requests: requests.map((r)=>({
|
|
777
|
+
...r,
|
|
778
|
+
// Avoid logging large blob data
|
|
779
|
+
blobConfig: r.blobConfig ? {
|
|
780
|
+
...r.blobConfig,
|
|
781
|
+
blobs: r.blobConfig.blobs.map((b)=>({
|
|
782
|
+
size: trimmedBytesLength(b)
|
|
783
|
+
}))
|
|
784
|
+
} : undefined
|
|
785
|
+
}))
|
|
657
786
|
});
|
|
658
787
|
const successfulActions = [];
|
|
659
788
|
const failedActions = [];
|
|
@@ -664,6 +793,37 @@ export class SequencerPublisher {
|
|
|
664
793
|
failedActions.push(request.action);
|
|
665
794
|
}
|
|
666
795
|
}
|
|
796
|
+
// Single backup for the whole reverted tx
|
|
797
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
798
|
+
this.backupFailedTx({
|
|
799
|
+
id: result.receipt.transactionHash,
|
|
800
|
+
failureType: 'revert',
|
|
801
|
+
request: {
|
|
802
|
+
to: MULTI_CALL_3_ADDRESS,
|
|
803
|
+
data: txContext.multicallData
|
|
804
|
+
},
|
|
805
|
+
blobData: txContext.blobData,
|
|
806
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
807
|
+
receipt: {
|
|
808
|
+
transactionHash: result.receipt.transactionHash,
|
|
809
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
810
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
811
|
+
status: 'reverted'
|
|
812
|
+
},
|
|
813
|
+
error: {
|
|
814
|
+
message: result.errorMsg ?? 'Transaction reverted'
|
|
815
|
+
},
|
|
816
|
+
context: {
|
|
817
|
+
actions: failedActions,
|
|
818
|
+
requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
|
|
819
|
+
action: r.action,
|
|
820
|
+
to: r.request.to,
|
|
821
|
+
data: r.request.data
|
|
822
|
+
})),
|
|
823
|
+
sender: this.getSenderAddress().toString()
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
667
827
|
return {
|
|
668
828
|
successfulActions,
|
|
669
829
|
failedActions
|
|
@@ -671,17 +831,20 @@ export class SequencerPublisher {
|
|
|
671
831
|
}
|
|
672
832
|
}
|
|
673
833
|
/**
|
|
674
|
-
* @notice Will call `
|
|
834
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
675
835
|
* @param tipArchive - The archive to check
|
|
676
836
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
677
|
-
*/
|
|
837
|
+
*/ async canProposeAt(tipArchive, msgSender, opts = {}) {
|
|
678
838
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
679
839
|
const ignoredErrors = [
|
|
680
840
|
'SlotAlreadyInChain',
|
|
681
841
|
'InvalidProposer',
|
|
682
842
|
'InvalidArchive'
|
|
683
843
|
];
|
|
684
|
-
|
|
844
|
+
const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
|
|
845
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
846
|
+
const nextL1SlotTs = await this.getNextL1SlotTimestampWithL1Floor() + slotOffset;
|
|
847
|
+
return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
685
848
|
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
|
|
686
849
|
}).catch((err)=>{
|
|
687
850
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
@@ -713,7 +876,7 @@ export class SequencerPublisher {
|
|
|
713
876
|
header.blobsHash.toString(),
|
|
714
877
|
flags
|
|
715
878
|
];
|
|
716
|
-
const ts =
|
|
879
|
+
const ts = await this.getNextL1SlotTimestampWithL1Floor();
|
|
717
880
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
|
|
718
881
|
let balance = 0n;
|
|
719
882
|
if (this.config.fishermanMode) {
|
|
@@ -766,6 +929,7 @@ export class SequencerPublisher {
|
|
|
766
929
|
...logData,
|
|
767
930
|
request
|
|
768
931
|
});
|
|
932
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
769
933
|
try {
|
|
770
934
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
|
|
771
935
|
request.abi ?? [],
|
|
@@ -808,6 +972,27 @@ export class SequencerPublisher {
|
|
|
808
972
|
}
|
|
809
973
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
810
974
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
975
|
+
this.backupFailedTx({
|
|
976
|
+
id: keccak256(request.data),
|
|
977
|
+
failureType: 'simulation',
|
|
978
|
+
request: {
|
|
979
|
+
to: request.to,
|
|
980
|
+
data: request.data,
|
|
981
|
+
value: request.value?.toString()
|
|
982
|
+
},
|
|
983
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
984
|
+
error: {
|
|
985
|
+
message: viemError.message,
|
|
986
|
+
name: viemError.name
|
|
987
|
+
},
|
|
988
|
+
context: {
|
|
989
|
+
actions: [
|
|
990
|
+
`invalidate-${reason}`
|
|
991
|
+
],
|
|
992
|
+
checkpointNumber,
|
|
993
|
+
sender: this.getSenderAddress().toString()
|
|
994
|
+
}
|
|
995
|
+
});
|
|
811
996
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
|
|
812
997
|
cause: viemError
|
|
813
998
|
});
|
|
@@ -834,30 +1019,18 @@ export class SequencerPublisher {
|
|
|
834
1019
|
}
|
|
835
1020
|
}
|
|
836
1021
|
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
|
|
837
|
-
|
|
838
|
-
//
|
|
839
|
-
|
|
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
|
-
// }
|
|
1022
|
+
// Anchor the simulation timestamp to the checkpoint's own slot start time
|
|
1023
|
+
// rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
|
|
1024
|
+
const ts = checkpoint.header.timestamp;
|
|
852
1025
|
const blobFields = checkpoint.toBlobFields();
|
|
853
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1026
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
854
1027
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
855
1028
|
const args = [
|
|
856
1029
|
{
|
|
857
1030
|
header: checkpoint.header.toViem(),
|
|
858
1031
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
859
1032
|
oracleInput: {
|
|
860
|
-
feeAssetPriceModifier:
|
|
1033
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
861
1034
|
}
|
|
862
1035
|
},
|
|
863
1036
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -892,6 +1065,28 @@ export class SequencerPublisher {
|
|
|
892
1065
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
893
1066
|
return false;
|
|
894
1067
|
}
|
|
1068
|
+
// Check if payload was already submitted to governance
|
|
1069
|
+
const cacheKey = payload.toString();
|
|
1070
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
1071
|
+
try {
|
|
1072
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
1073
|
+
const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
|
|
1074
|
+
0,
|
|
1075
|
+
1,
|
|
1076
|
+
2
|
|
1077
|
+
]), this.log, true);
|
|
1078
|
+
if (proposed) {
|
|
1079
|
+
this.payloadProposedCache.add(cacheKey);
|
|
1080
|
+
}
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
1087
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
895
1090
|
const cachedLastVote = this.lastActions[signalType];
|
|
896
1091
|
this.lastActions[signalType] = slotNumber;
|
|
897
1092
|
const action = signalType;
|
|
@@ -902,6 +1097,7 @@ export class SequencerPublisher {
|
|
|
902
1097
|
signer: this.l1TxUtils.client.account?.address,
|
|
903
1098
|
lastValidL2Slot: slotNumber
|
|
904
1099
|
});
|
|
1100
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
905
1101
|
try {
|
|
906
1102
|
await this.l1TxUtils.simulate(request, {
|
|
907
1103
|
time: timestamp
|
|
@@ -913,7 +1109,29 @@ export class SequencerPublisher {
|
|
|
913
1109
|
request
|
|
914
1110
|
});
|
|
915
1111
|
} catch (err) {
|
|
916
|
-
|
|
1112
|
+
const viemError = formatViemError(err);
|
|
1113
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
1114
|
+
this.backupFailedTx({
|
|
1115
|
+
id: keccak256(request.data),
|
|
1116
|
+
failureType: 'simulation',
|
|
1117
|
+
request: {
|
|
1118
|
+
to: request.to,
|
|
1119
|
+
data: request.data,
|
|
1120
|
+
value: request.value?.toString()
|
|
1121
|
+
},
|
|
1122
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1123
|
+
error: {
|
|
1124
|
+
message: viemError.message,
|
|
1125
|
+
name: viemError.name
|
|
1126
|
+
},
|
|
1127
|
+
context: {
|
|
1128
|
+
actions: [
|
|
1129
|
+
action
|
|
1130
|
+
],
|
|
1131
|
+
slot: slotNumber,
|
|
1132
|
+
sender: this.getSenderAddress().toString()
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
917
1135
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
918
1136
|
}
|
|
919
1137
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
@@ -1052,13 +1270,14 @@ export class SequencerPublisher {
|
|
|
1052
1270
|
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
1053
1271
|
const checkpointHeader = checkpoint.header;
|
|
1054
1272
|
const blobFields = checkpoint.toBlobFields();
|
|
1055
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1273
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
1056
1274
|
const proposeTxArgs = {
|
|
1057
1275
|
header: checkpointHeader,
|
|
1058
1276
|
archive: checkpoint.archive.root.toBuffer(),
|
|
1059
1277
|
blobs,
|
|
1060
1278
|
attestationsAndSigners,
|
|
1061
|
-
attestationsAndSignersSignature
|
|
1279
|
+
attestationsAndSignersSignature,
|
|
1280
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
|
|
1062
1281
|
};
|
|
1063
1282
|
let ts;
|
|
1064
1283
|
try {
|
|
@@ -1134,6 +1353,7 @@ export class SequencerPublisher {
|
|
|
1134
1353
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1135
1354
|
this.lastActions[action] = slotNumber;
|
|
1136
1355
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1356
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1137
1357
|
let gasUsed;
|
|
1138
1358
|
const simulateAbi = mergeAbis([
|
|
1139
1359
|
request.abi ?? [],
|
|
@@ -1151,6 +1371,27 @@ export class SequencerPublisher {
|
|
|
1151
1371
|
} catch (err) {
|
|
1152
1372
|
const viemError = formatViemError(err, simulateAbi);
|
|
1153
1373
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1374
|
+
this.backupFailedTx({
|
|
1375
|
+
id: keccak256(request.data),
|
|
1376
|
+
failureType: 'simulation',
|
|
1377
|
+
request: {
|
|
1378
|
+
to: request.to,
|
|
1379
|
+
data: request.data,
|
|
1380
|
+
value: request.value?.toString()
|
|
1381
|
+
},
|
|
1382
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1383
|
+
error: {
|
|
1384
|
+
message: viemError.message,
|
|
1385
|
+
name: viemError.name
|
|
1386
|
+
},
|
|
1387
|
+
context: {
|
|
1388
|
+
actions: [
|
|
1389
|
+
action
|
|
1390
|
+
],
|
|
1391
|
+
slot: slotNumber,
|
|
1392
|
+
sender: this.getSenderAddress().toString()
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1154
1395
|
return false;
|
|
1155
1396
|
}
|
|
1156
1397
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
@@ -1229,10 +1470,38 @@ export class SequencerPublisher {
|
|
|
1229
1470
|
}, {}, {
|
|
1230
1471
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
1231
1472
|
kzg
|
|
1232
|
-
}).catch((err)=>{
|
|
1233
|
-
const
|
|
1234
|
-
this.log.error(`Failed to validate blobs`, message, {
|
|
1235
|
-
metaMessages
|
|
1473
|
+
}).catch(async (err)=>{
|
|
1474
|
+
const viemError = formatViemError(err);
|
|
1475
|
+
this.log.error(`Failed to validate blobs`, viemError.message, {
|
|
1476
|
+
metaMessages: viemError.metaMessages
|
|
1477
|
+
});
|
|
1478
|
+
const validateBlobsData = encodeFunctionData({
|
|
1479
|
+
abi: RollupAbi,
|
|
1480
|
+
functionName: 'validateBlobs',
|
|
1481
|
+
args: [
|
|
1482
|
+
blobInput
|
|
1483
|
+
]
|
|
1484
|
+
});
|
|
1485
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1486
|
+
this.backupFailedTx({
|
|
1487
|
+
id: keccak256(validateBlobsData),
|
|
1488
|
+
failureType: 'simulation',
|
|
1489
|
+
request: {
|
|
1490
|
+
to: this.rollupContract.address,
|
|
1491
|
+
data: validateBlobsData
|
|
1492
|
+
},
|
|
1493
|
+
blobData: encodedData.blobs.map((b)=>toHex(b.data)),
|
|
1494
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1495
|
+
error: {
|
|
1496
|
+
message: viemError.message,
|
|
1497
|
+
name: viemError.name
|
|
1498
|
+
},
|
|
1499
|
+
context: {
|
|
1500
|
+
actions: [
|
|
1501
|
+
'validate-blobs'
|
|
1502
|
+
],
|
|
1503
|
+
sender: this.getSenderAddress().toString()
|
|
1504
|
+
}
|
|
1236
1505
|
});
|
|
1237
1506
|
throw new Error('Failed to validate blobs');
|
|
1238
1507
|
});
|
|
@@ -1243,8 +1512,7 @@ export class SequencerPublisher {
|
|
|
1243
1512
|
header: encodedData.header.toViem(),
|
|
1244
1513
|
archive: toHex(encodedData.archive),
|
|
1245
1514
|
oracleInput: {
|
|
1246
|
-
|
|
1247
|
-
feeAssetPriceModifier: 0n
|
|
1515
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier
|
|
1248
1516
|
}
|
|
1249
1517
|
},
|
|
1250
1518
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1293,6 +1561,7 @@ export class SequencerPublisher {
|
|
|
1293
1561
|
balance: 10n * WEI_CONST * WEI_CONST
|
|
1294
1562
|
});
|
|
1295
1563
|
}
|
|
1564
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1296
1565
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
1297
1566
|
to: this.rollupContract.address,
|
|
1298
1567
|
data: rollupData,
|
|
@@ -1320,6 +1589,26 @@ export class SequencerPublisher {
|
|
|
1320
1589
|
};
|
|
1321
1590
|
}
|
|
1322
1591
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1592
|
+
this.backupFailedTx({
|
|
1593
|
+
id: keccak256(rollupData),
|
|
1594
|
+
failureType: 'simulation',
|
|
1595
|
+
request: {
|
|
1596
|
+
to: this.rollupContract.address,
|
|
1597
|
+
data: rollupData
|
|
1598
|
+
},
|
|
1599
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1600
|
+
error: {
|
|
1601
|
+
message: viemError.message,
|
|
1602
|
+
name: viemError.name
|
|
1603
|
+
},
|
|
1604
|
+
context: {
|
|
1605
|
+
actions: [
|
|
1606
|
+
'propose'
|
|
1607
|
+
],
|
|
1608
|
+
slot: Number(args[0].header.slotNumber),
|
|
1609
|
+
sender: this.getSenderAddress().toString()
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1323
1612
|
throw err;
|
|
1324
1613
|
});
|
|
1325
1614
|
return {
|
|
@@ -1396,4 +1685,19 @@ export class SequencerPublisher {
|
|
|
1396
1685
|
}
|
|
1397
1686
|
});
|
|
1398
1687
|
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Returns the timestamp to use when simulating L1 proposal calls.
|
|
1690
|
+
* Uses the wall-clock-based next L1 slot boundary, but floors it with the latest L1 block timestamp
|
|
1691
|
+
* plus one slot duration. This prevents the sequencer from targeting a future L2 slot when the L1
|
|
1692
|
+
* chain hasn't caught up to the wall clock yet (e.g., the dateProvider is one L1 slot ahead of the
|
|
1693
|
+
* latest mined block), which would cause the propose tx to land in an L1 block with block.timestamp
|
|
1694
|
+
* still in the previous L2 slot.
|
|
1695
|
+
* TODO(palla): Properly fix by keeping dateProvider synced with anvil's chain time on every block.
|
|
1696
|
+
*/ async getNextL1SlotTimestampWithL1Floor() {
|
|
1697
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1698
|
+
const fromWallClock = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1699
|
+
const latestBlock = await this.l1TxUtils.client.getBlock();
|
|
1700
|
+
const fromL1Block = latestBlock.timestamp + BigInt(l1Constants.ethereumSlotDuration);
|
|
1701
|
+
return fromWallClock > fromL1Block ? fromWallClock : fromL1Block;
|
|
1702
|
+
}
|
|
1399
1703
|
}
|