@aztec/sequencer-client 0.0.1-commit.5daedc8 → 0.0.1-commit.6201a7b05
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 +26 -16
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +76 -36
- package/dest/config.d.ts +35 -9
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +113 -50
- 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 +25 -17
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +44 -68
- 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/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/publisher/config.d.ts +53 -20
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +124 -39
- 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 +15 -8
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +28 -4
- package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +23 -86
- package/dest/publisher/sequencer-publisher.d.ts +114 -80
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +1010 -244
- 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 +138 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job.js +1665 -0
- 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 +34 -0
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_voter.js +106 -0
- package/dest/sequencer/config.d.ts +3 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/events.d.ts +52 -0
- package/dest/sequencer/events.d.ts.map +1 -0
- package/dest/sequencer/events.js +1 -0
- package/dest/sequencer/index.d.ts +4 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +3 -1
- package/dest/sequencer/metrics.d.ts +42 -7
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +241 -72
- package/dest/sequencer/sequencer.d.ts +146 -135
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +841 -661
- package/dest/sequencer/timetable.d.ts +67 -16
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +158 -64
- package/dest/sequencer/types.d.ts +3 -0
- package/dest/sequencer/types.d.ts.map +1 -0
- package/dest/sequencer/types.js +1 -0
- package/dest/sequencer/utils.d.ts +14 -8
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +7 -4
- package/dest/test/index.d.ts +6 -7
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +95 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +231 -0
- package/dest/test/utils.d.ts +53 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +105 -0
- package/package.json +31 -30
- package/src/client/sequencer-client.ts +102 -62
- package/src/config.ts +132 -59
- 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 +64 -96
- package/src/global_variable_builder/index.ts +3 -1
- package/src/index.ts +1 -7
- package/src/publisher/config.ts +166 -51
- 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 +43 -13
- package/src/publisher/sequencer-publisher-metrics.ts +19 -71
- package/src/publisher/sequencer-publisher.ts +763 -325
- package/src/sequencer/README.md +600 -0
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +1498 -0
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +119 -0
- package/src/sequencer/config.ts +2 -1
- package/src/sequencer/events.ts +32 -0
- package/src/sequencer/index.ts +3 -1
- package/src/sequencer/metrics.ts +302 -83
- package/src/sequencer/sequencer.ts +641 -863
- package/src/sequencer/timetable.ts +188 -81
- package/src/sequencer/types.ts +6 -0
- package/src/sequencer/utils.ts +18 -9
- package/src/test/index.ts +5 -6
- package/src/test/mock_checkpoint_builder.ts +323 -0
- package/src/test/utils.ts +185 -0
- package/dest/sequencer/block_builder.d.ts +0 -27
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -134
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -53
- package/src/sequencer/block_builder.ts +0 -222
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -132
|
@@ -1,50 +1,82 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
3
|
-
import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
|
|
4
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
FormattedViemError,
|
|
6
|
+
FeeAssetPriceOracle,
|
|
8
7
|
type GovernanceProposerContract,
|
|
9
8
|
type IEmpireBase,
|
|
10
|
-
type L1BlobInputs,
|
|
11
|
-
type L1ContractsConfig,
|
|
12
|
-
type L1TxConfig,
|
|
13
|
-
type L1TxRequest,
|
|
14
9
|
MULTI_CALL_3_ADDRESS,
|
|
15
10
|
Multicall3,
|
|
16
11
|
RollupContract,
|
|
17
|
-
|
|
18
|
-
type
|
|
12
|
+
SimulationOverridesBuilder,
|
|
13
|
+
type SimulationOverridesPlan,
|
|
14
|
+
type SlashingProposerContract,
|
|
19
15
|
type ViemCommitteeAttestations,
|
|
20
16
|
type ViemHeader,
|
|
17
|
+
buildSimulationOverridesStateOverride,
|
|
18
|
+
} from '@aztec/ethereum/contracts';
|
|
19
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
20
|
+
import {
|
|
21
|
+
type L1BlobInputs,
|
|
22
|
+
type L1TxConfig,
|
|
23
|
+
type L1TxRequest,
|
|
24
|
+
type L1TxUtils,
|
|
25
|
+
MAX_L1_TX_LIMIT,
|
|
26
|
+
type TransactionStats,
|
|
21
27
|
WEI_CONST,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from '@aztec/ethereum';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
28
|
+
} from '@aztec/ethereum/l1-tx-utils';
|
|
29
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
30
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
31
|
+
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
32
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
33
|
+
import { pick } from '@aztec/foundation/collection';
|
|
34
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
29
36
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
30
37
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
31
|
-
import type { Fr } from '@aztec/foundation/fields';
|
|
32
38
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
39
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
33
41
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
34
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
42
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
35
43
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
36
44
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
45
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
46
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
47
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
39
48
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
40
|
-
import type {
|
|
41
|
-
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
42
|
-
|
|
43
|
-
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
49
|
+
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
50
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
44
51
|
|
|
45
|
-
import
|
|
52
|
+
import {
|
|
53
|
+
type Hex,
|
|
54
|
+
type TransactionReceipt,
|
|
55
|
+
type TypedDataDefinition,
|
|
56
|
+
encodeFunctionData,
|
|
57
|
+
keccak256,
|
|
58
|
+
multicall3Abi,
|
|
59
|
+
toHex,
|
|
60
|
+
} from 'viem';
|
|
61
|
+
|
|
62
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
63
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
46
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
47
65
|
|
|
66
|
+
/** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
|
|
67
|
+
export type SendRequestsResult = {
|
|
68
|
+
/** The L1 transaction receipt or error from the bundled multicall. */
|
|
69
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
|
|
70
|
+
/** Actions that expired (past their deadline) before the request was sent. */
|
|
71
|
+
expiredActions: Action[];
|
|
72
|
+
/** Actions that were included in the sent L1 transaction. */
|
|
73
|
+
sentActions: Action[];
|
|
74
|
+
/** Actions whose L1 simulation succeeded (subset of sentActions). */
|
|
75
|
+
successfulActions: Action[];
|
|
76
|
+
/** Actions whose L1 simulation failed (subset of sentActions). */
|
|
77
|
+
failedActions: Action[];
|
|
78
|
+
};
|
|
79
|
+
|
|
48
80
|
/** Arguments to the process method of the rollup contract */
|
|
49
81
|
type L1ProcessArgs = {
|
|
50
82
|
/** The L2 block header. */
|
|
@@ -57,6 +89,8 @@ type L1ProcessArgs = {
|
|
|
57
89
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
58
90
|
/** Attestations and signers signature */
|
|
59
91
|
attestationsAndSignersSignature: Signature;
|
|
92
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
93
|
+
feeAssetPriceModifier: bigint;
|
|
60
94
|
};
|
|
61
95
|
|
|
62
96
|
export const Actions = [
|
|
@@ -64,26 +98,30 @@ export const Actions = [
|
|
|
64
98
|
'invalidate-by-insufficient-attestations',
|
|
65
99
|
'propose',
|
|
66
100
|
'governance-signal',
|
|
67
|
-
'empire-slashing-signal',
|
|
68
|
-
'create-empire-payload',
|
|
69
|
-
'execute-empire-payload',
|
|
70
101
|
'vote-offenses',
|
|
71
102
|
'execute-slash',
|
|
72
103
|
] as const;
|
|
73
104
|
|
|
74
105
|
export type Action = (typeof Actions)[number];
|
|
75
106
|
|
|
76
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
107
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
77
108
|
|
|
78
109
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
79
110
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
80
111
|
|
|
81
|
-
export type
|
|
112
|
+
export type InvalidateCheckpointRequest = {
|
|
82
113
|
request: L1TxRequest;
|
|
83
114
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
84
115
|
gasUsed: bigint;
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
checkpointNumber: CheckpointNumber;
|
|
117
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
118
|
+
/** Archive at the rollback target checkpoint (checkpoint N-1). */
|
|
119
|
+
lastArchive: Fr;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type EnqueueProposeCheckpointOpts = {
|
|
123
|
+
txTimeoutAt?: Date;
|
|
124
|
+
simulationOverridesPlan?: SimulationOverridesPlan;
|
|
87
125
|
};
|
|
88
126
|
|
|
89
127
|
interface RequestWithExpiry {
|
|
@@ -92,6 +130,8 @@ interface RequestWithExpiry {
|
|
|
92
130
|
lastValidL2Slot: SlotNumber;
|
|
93
131
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
94
132
|
blobConfig?: L1BlobInputs;
|
|
133
|
+
/** Optional pre-send validation. If it rejects, the request is discarded. */
|
|
134
|
+
preCheck?: () => Promise<void>;
|
|
95
135
|
checkSuccess: (
|
|
96
136
|
request: L1TxRequest,
|
|
97
137
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -102,23 +142,39 @@ export class SequencerPublisher {
|
|
|
102
142
|
private interrupted = false;
|
|
103
143
|
private metrics: SequencerPublisherMetrics;
|
|
104
144
|
public epochCache: EpochCache;
|
|
145
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
105
146
|
|
|
106
147
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
107
148
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
108
149
|
|
|
109
150
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
110
151
|
|
|
152
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
153
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
154
|
+
|
|
111
155
|
protected log: Logger;
|
|
112
156
|
protected ethereumSlotDuration: bigint;
|
|
157
|
+
protected aztecSlotDuration: bigint;
|
|
158
|
+
|
|
159
|
+
/** Date provider for wall-clock time. */
|
|
160
|
+
private readonly dateProvider: DateProvider;
|
|
113
161
|
|
|
114
|
-
private
|
|
162
|
+
private blobClient: BlobClientInterface;
|
|
115
163
|
|
|
116
164
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
117
165
|
private proposerAddressForSimulation?: EthAddress;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
|
|
167
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
168
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
169
|
+
|
|
170
|
+
/** L1 fee analyzer for fisherman mode */
|
|
171
|
+
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
172
|
+
|
|
173
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
174
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
175
|
+
|
|
176
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
177
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
122
178
|
|
|
123
179
|
// A CALL to a cold address is 2700 gas
|
|
124
180
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -126,42 +182,48 @@ export class SequencerPublisher {
|
|
|
126
182
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
127
183
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
128
184
|
|
|
129
|
-
public l1TxUtils:
|
|
185
|
+
public l1TxUtils: L1TxUtils;
|
|
130
186
|
public rollupContract: RollupContract;
|
|
131
187
|
public govProposerContract: GovernanceProposerContract;
|
|
132
|
-
public slashingProposerContract:
|
|
133
|
-
|
|
188
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
189
|
+
|
|
190
|
+
public readonly tracer: Tracer;
|
|
134
191
|
|
|
135
192
|
protected requests: RequestWithExpiry[] = [];
|
|
136
193
|
|
|
137
194
|
constructor(
|
|
138
|
-
private config:
|
|
195
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
196
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
139
197
|
deps: {
|
|
140
198
|
telemetry?: TelemetryClient;
|
|
141
|
-
|
|
142
|
-
l1TxUtils:
|
|
199
|
+
blobClient: BlobClientInterface;
|
|
200
|
+
l1TxUtils: L1TxUtils;
|
|
143
201
|
rollupContract: RollupContract;
|
|
144
|
-
slashingProposerContract:
|
|
202
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
145
203
|
governanceProposerContract: GovernanceProposerContract;
|
|
146
|
-
slashFactoryContract: SlashFactoryContract;
|
|
147
204
|
epochCache: EpochCache;
|
|
148
205
|
dateProvider: DateProvider;
|
|
149
206
|
metrics: SequencerPublisherMetrics;
|
|
150
207
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
151
208
|
log?: Logger;
|
|
209
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
152
210
|
},
|
|
153
211
|
) {
|
|
154
212
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
155
213
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
214
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
215
|
+
this.dateProvider = deps.dateProvider;
|
|
156
216
|
this.epochCache = deps.epochCache;
|
|
157
217
|
this.lastActions = deps.lastActions;
|
|
158
218
|
|
|
159
|
-
this.
|
|
160
|
-
|
|
219
|
+
this.blobClient = deps.blobClient;
|
|
220
|
+
this.dateProvider = deps.dateProvider;
|
|
161
221
|
|
|
162
222
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
163
223
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
224
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
164
225
|
this.l1TxUtils = deps.l1TxUtils;
|
|
226
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
165
227
|
|
|
166
228
|
this.rollupContract = deps.rollupContract;
|
|
167
229
|
|
|
@@ -173,17 +235,71 @@ export class SequencerPublisher {
|
|
|
173
235
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
174
236
|
this.slashingProposerContract = newSlashingProposer;
|
|
175
237
|
});
|
|
176
|
-
|
|
238
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
239
|
+
if (config.fishermanMode) {
|
|
240
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
241
|
+
this.l1TxUtils.client,
|
|
242
|
+
deps.dateProvider,
|
|
243
|
+
createLogger('sequencer:publisher:fee-analyzer'),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Initialize fee asset price oracle
|
|
248
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
249
|
+
this.l1TxUtils.client,
|
|
250
|
+
this.rollupContract,
|
|
251
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
255
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
260
|
+
* Does nothing if no store is configured.
|
|
261
|
+
*/
|
|
262
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
263
|
+
if (!this.failedTxStore) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tx: FailedL1Tx = {
|
|
268
|
+
...failedTx,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Fire and forget - don't block on backup
|
|
273
|
+
void this.failedTxStore
|
|
274
|
+
.then(store => store?.saveFailedTx(tx))
|
|
275
|
+
.catch(err => {
|
|
276
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
277
|
+
});
|
|
177
278
|
}
|
|
178
279
|
|
|
179
280
|
public getRollupContract(): RollupContract {
|
|
180
281
|
return this.rollupContract;
|
|
181
282
|
}
|
|
182
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Gets the fee asset price modifier from the oracle.
|
|
286
|
+
* Returns 0n if the oracle query fails.
|
|
287
|
+
*/
|
|
288
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
289
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
290
|
+
}
|
|
291
|
+
|
|
183
292
|
public getSenderAddress() {
|
|
184
293
|
return this.l1TxUtils.getSenderAddress();
|
|
185
294
|
}
|
|
186
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
298
|
+
*/
|
|
299
|
+
public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
|
|
300
|
+
return this.l1FeeAnalyzer;
|
|
301
|
+
}
|
|
302
|
+
|
|
187
303
|
/**
|
|
188
304
|
* Sets the proposer address to use for simulations in fisherman mode.
|
|
189
305
|
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
@@ -197,7 +313,7 @@ export class SequencerPublisher {
|
|
|
197
313
|
}
|
|
198
314
|
|
|
199
315
|
public getCurrentL2Slot(): SlotNumber {
|
|
200
|
-
return this.epochCache.
|
|
316
|
+
return this.epochCache.getSlotNow();
|
|
201
317
|
}
|
|
202
318
|
|
|
203
319
|
/**
|
|
@@ -211,6 +327,62 @@ export class SequencerPublisher {
|
|
|
211
327
|
}
|
|
212
328
|
}
|
|
213
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
332
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
333
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
334
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
335
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
336
|
+
*/
|
|
337
|
+
public async analyzeL1Fees(
|
|
338
|
+
l2SlotNumber: SlotNumber,
|
|
339
|
+
onComplete?: (analysis: L1FeeAnalysisResult) => void,
|
|
340
|
+
): Promise<L1FeeAnalysisResult | undefined> {
|
|
341
|
+
if (!this.l1FeeAnalyzer) {
|
|
342
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const requestsToAnalyze = [...this.requests];
|
|
347
|
+
if (requestsToAnalyze.length === 0) {
|
|
348
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Extract blob config from requests (if any)
|
|
353
|
+
const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
354
|
+
const blobConfig = blobConfigs[0];
|
|
355
|
+
|
|
356
|
+
// Get gas configs
|
|
357
|
+
const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
358
|
+
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
359
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
|
|
360
|
+
|
|
361
|
+
// Get the transaction requests
|
|
362
|
+
const l1Requests = requestsToAnalyze.map(r => r.request);
|
|
363
|
+
|
|
364
|
+
// Start the analysis
|
|
365
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
366
|
+
l2SlotNumber,
|
|
367
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
368
|
+
l1Requests,
|
|
369
|
+
blobConfig,
|
|
370
|
+
onComplete,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
this.log.info('Started L1 fee analysis', {
|
|
374
|
+
analysisId,
|
|
375
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
376
|
+
requestCount: requestsToAnalyze.length,
|
|
377
|
+
hasBlobConfig: !!blobConfig,
|
|
378
|
+
gasLimit: gasLimit.toString(),
|
|
379
|
+
actions: requestsToAnalyze.map(r => r.action),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
383
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
384
|
+
}
|
|
385
|
+
|
|
214
386
|
/**
|
|
215
387
|
* Sends all requests that are still valid.
|
|
216
388
|
* @returns one of:
|
|
@@ -218,10 +390,12 @@ export class SequencerPublisher {
|
|
|
218
390
|
* - a receipt and errorMsg if it failed on L1
|
|
219
391
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
220
392
|
*/
|
|
221
|
-
|
|
393
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
394
|
+
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
222
395
|
const requestsToProcess = [...this.requests];
|
|
223
396
|
this.requests = [];
|
|
224
|
-
|
|
397
|
+
|
|
398
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
225
399
|
return undefined;
|
|
226
400
|
}
|
|
227
401
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -253,8 +427,8 @@ export class SequencerPublisher {
|
|
|
253
427
|
// @note - we can only have one blob config per bundle
|
|
254
428
|
// find requests with gas and blob configs
|
|
255
429
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
256
|
-
const gasConfigs =
|
|
257
|
-
const blobConfigs =
|
|
430
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
431
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
258
432
|
|
|
259
433
|
if (blobConfigs.length > 1) {
|
|
260
434
|
throw new Error('Multiple blob configs found');
|
|
@@ -264,7 +438,16 @@ export class SequencerPublisher {
|
|
|
264
438
|
|
|
265
439
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
266
440
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
267
|
-
|
|
441
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
442
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
443
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
444
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
445
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
446
|
+
requested: gasLimit,
|
|
447
|
+
capped: maxGas,
|
|
448
|
+
});
|
|
449
|
+
gasLimit = maxGas;
|
|
450
|
+
}
|
|
268
451
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
269
452
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
270
453
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -274,19 +457,36 @@ export class SequencerPublisher {
|
|
|
274
457
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
275
458
|
|
|
276
459
|
try {
|
|
460
|
+
// Capture context for failed tx backup before sending
|
|
461
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
462
|
+
const multicallData = encodeFunctionData({
|
|
463
|
+
abi: multicall3Abi,
|
|
464
|
+
functionName: 'aggregate3',
|
|
465
|
+
args: [
|
|
466
|
+
validRequests.map(r => ({
|
|
467
|
+
target: r.request.to!,
|
|
468
|
+
callData: r.request.data!,
|
|
469
|
+
allowFailure: true,
|
|
470
|
+
})),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
474
|
+
|
|
475
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
476
|
+
|
|
277
477
|
this.log.debug('Forwarding transactions', {
|
|
278
478
|
validRequests: validRequests.map(request => request.action),
|
|
279
479
|
txConfig,
|
|
280
480
|
});
|
|
281
|
-
const result = await
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
481
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
482
|
+
if (result === undefined) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
486
|
+
validRequests,
|
|
487
|
+
result,
|
|
488
|
+
txContext,
|
|
288
489
|
);
|
|
289
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
290
490
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
291
491
|
} catch (err) {
|
|
292
492
|
const viemError = formatViemError(err);
|
|
@@ -304,16 +504,127 @@ export class SequencerPublisher {
|
|
|
304
504
|
}
|
|
305
505
|
}
|
|
306
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
509
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
510
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
511
|
+
*/
|
|
512
|
+
private async forwardWithPublisherRotation(
|
|
513
|
+
validRequests: RequestWithExpiry[],
|
|
514
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
515
|
+
blobConfig: L1BlobInputs | undefined,
|
|
516
|
+
) {
|
|
517
|
+
const triedAddresses: EthAddress[] = [];
|
|
518
|
+
let currentPublisher = this.l1TxUtils;
|
|
519
|
+
|
|
520
|
+
while (true) {
|
|
521
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
522
|
+
try {
|
|
523
|
+
const result = await Multicall3.forward(
|
|
524
|
+
validRequests.map(r => r.request),
|
|
525
|
+
currentPublisher,
|
|
526
|
+
txConfig,
|
|
527
|
+
blobConfig,
|
|
528
|
+
this.rollupContract.address,
|
|
529
|
+
this.log,
|
|
530
|
+
);
|
|
531
|
+
this.l1TxUtils = currentPublisher;
|
|
532
|
+
return result;
|
|
533
|
+
} catch (err) {
|
|
534
|
+
if (err instanceof TimeoutError) {
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
537
|
+
const viemError = formatViemError(err);
|
|
538
|
+
if (!this.getNextPublisher) {
|
|
539
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
this.log.warn(
|
|
543
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
544
|
+
viemError,
|
|
545
|
+
);
|
|
546
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
547
|
+
if (!nextPublisher) {
|
|
548
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
currentPublisher = nextPublisher;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/*
|
|
557
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
558
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
559
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
560
|
+
*/
|
|
561
|
+
public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
|
|
562
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
563
|
+
if (ms > 0) {
|
|
564
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
|
|
565
|
+
await this.interruptibleSleep.sleep(ms);
|
|
566
|
+
}
|
|
567
|
+
if (this.interrupted) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
|
|
572
|
+
const validRequests: RequestWithExpiry[] = [];
|
|
573
|
+
for (const request of this.requests) {
|
|
574
|
+
if (!request.preCheck) {
|
|
575
|
+
validRequests.push(request);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
await request.preCheck();
|
|
581
|
+
validRequests.push(request);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.requests = validRequests;
|
|
588
|
+
if (this.requests.length === 0) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return this.sendRequests();
|
|
593
|
+
}
|
|
594
|
+
|
|
307
595
|
private callbackBundledTransactions(
|
|
308
596
|
requests: RequestWithExpiry[],
|
|
309
|
-
result
|
|
597
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
598
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
310
599
|
) {
|
|
311
600
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
312
601
|
if (result instanceof FormattedViemError) {
|
|
313
602
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
603
|
+
this.backupFailedTx({
|
|
604
|
+
id: keccak256(txContext.multicallData),
|
|
605
|
+
failureType: 'send-error',
|
|
606
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
607
|
+
blobData: txContext.blobData,
|
|
608
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
609
|
+
error: { message: result.message, name: result.name },
|
|
610
|
+
context: {
|
|
611
|
+
actions: requests.map(r => r.action),
|
|
612
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
613
|
+
sender: this.getSenderAddress().toString(),
|
|
614
|
+
},
|
|
615
|
+
});
|
|
314
616
|
return { failedActions: requests.map(r => r.action) };
|
|
315
617
|
} else {
|
|
316
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
618
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
619
|
+
result,
|
|
620
|
+
requests: requests.map(r => ({
|
|
621
|
+
...r,
|
|
622
|
+
// Avoid logging large blob data
|
|
623
|
+
blobConfig: r.blobConfig
|
|
624
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
625
|
+
: undefined,
|
|
626
|
+
})),
|
|
627
|
+
});
|
|
317
628
|
const successfulActions: Action[] = [];
|
|
318
629
|
const failedActions: Action[] = [];
|
|
319
630
|
for (const request of requests) {
|
|
@@ -323,27 +634,54 @@ export class SequencerPublisher {
|
|
|
323
634
|
failedActions.push(request.action);
|
|
324
635
|
}
|
|
325
636
|
}
|
|
637
|
+
// Single backup for the whole reverted tx
|
|
638
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
639
|
+
this.backupFailedTx({
|
|
640
|
+
id: result.receipt.transactionHash,
|
|
641
|
+
failureType: 'revert',
|
|
642
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
643
|
+
blobData: txContext.blobData,
|
|
644
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
645
|
+
receipt: {
|
|
646
|
+
transactionHash: result.receipt.transactionHash,
|
|
647
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
648
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
649
|
+
status: 'reverted',
|
|
650
|
+
},
|
|
651
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
652
|
+
context: {
|
|
653
|
+
actions: failedActions,
|
|
654
|
+
requests: requests
|
|
655
|
+
.filter(r => failedActions.includes(r.action))
|
|
656
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
657
|
+
sender: this.getSenderAddress().toString(),
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
326
661
|
return { successfulActions, failedActions };
|
|
327
662
|
}
|
|
328
663
|
}
|
|
329
664
|
|
|
330
665
|
/**
|
|
331
|
-
* @notice Will call `
|
|
666
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
332
667
|
* @param tipArchive - The archive to check
|
|
333
668
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
334
669
|
*/
|
|
335
|
-
public
|
|
336
|
-
tipArchive: Fr,
|
|
337
|
-
msgSender: EthAddress,
|
|
338
|
-
opts: { forcePendingBlockNumber?: number } = {},
|
|
339
|
-
) {
|
|
670
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
340
671
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
341
672
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
342
673
|
|
|
674
|
+
const pipelined = this.epochCache.isProposerPipeliningEnabled();
|
|
675
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
676
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
677
|
+
|
|
343
678
|
return this.rollupContract
|
|
344
|
-
.
|
|
345
|
-
|
|
346
|
-
|
|
679
|
+
.canProposeAt(
|
|
680
|
+
tipArchive.toBuffer(),
|
|
681
|
+
msgSender.toString(),
|
|
682
|
+
nextL1SlotTs,
|
|
683
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
684
|
+
)
|
|
347
685
|
.catch(err => {
|
|
348
686
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
349
687
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -355,27 +693,32 @@ export class SequencerPublisher {
|
|
|
355
693
|
return undefined;
|
|
356
694
|
});
|
|
357
695
|
}
|
|
696
|
+
|
|
358
697
|
/**
|
|
359
698
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
360
699
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
361
700
|
* It will throw if the block header is invalid.
|
|
362
701
|
* @param header - The block header to validate
|
|
363
702
|
*/
|
|
364
|
-
|
|
703
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
704
|
+
public async validateBlockHeader(
|
|
705
|
+
header: CheckpointHeader,
|
|
706
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
707
|
+
): Promise<void> {
|
|
365
708
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
366
709
|
|
|
367
710
|
const args = [
|
|
368
711
|
header.toViem(),
|
|
369
|
-
CommitteeAttestationsAndSigners.
|
|
712
|
+
CommitteeAttestationsAndSigners.packAttestations([]),
|
|
370
713
|
[], // no signers
|
|
371
714
|
Signature.empty().toViemSignature(),
|
|
372
715
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
373
|
-
header.
|
|
716
|
+
header.blobsHash.toString(),
|
|
374
717
|
flags,
|
|
375
718
|
] as const;
|
|
376
719
|
|
|
377
|
-
const ts =
|
|
378
|
-
const stateOverrides = await this.rollupContract
|
|
720
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
721
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
379
722
|
let balance = 0n;
|
|
380
723
|
if (this.config.fishermanMode) {
|
|
381
724
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -395,99 +738,130 @@ export class SequencerPublisher {
|
|
|
395
738
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
396
739
|
from: MULTI_CALL_3_ADDRESS,
|
|
397
740
|
},
|
|
398
|
-
{ time: ts
|
|
741
|
+
{ time: ts },
|
|
399
742
|
stateOverrides,
|
|
400
743
|
);
|
|
401
744
|
this.log.debug(`Simulated validateHeader`);
|
|
402
745
|
}
|
|
403
746
|
|
|
404
747
|
/**
|
|
405
|
-
* Simulate making a call to invalidate a
|
|
406
|
-
* @param
|
|
748
|
+
* Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
|
|
749
|
+
* @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
|
|
407
750
|
*/
|
|
408
|
-
public async
|
|
409
|
-
validationResult:
|
|
410
|
-
): Promise<
|
|
751
|
+
public async simulateInvalidateCheckpoint(
|
|
752
|
+
validationResult: ValidateCheckpointResult,
|
|
753
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
411
754
|
if (validationResult.valid) {
|
|
412
755
|
return undefined;
|
|
413
756
|
}
|
|
414
757
|
|
|
415
|
-
const { reason,
|
|
416
|
-
const
|
|
417
|
-
const logData = { ...
|
|
758
|
+
const { reason, checkpoint } = validationResult;
|
|
759
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
760
|
+
const logData = { ...checkpoint, reason };
|
|
418
761
|
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
762
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
763
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
421
764
|
this.log.verbose(
|
|
422
|
-
`Skipping
|
|
423
|
-
{
|
|
765
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
766
|
+
{ currentCheckpointNumber, ...logData },
|
|
424
767
|
);
|
|
425
768
|
return undefined;
|
|
426
769
|
}
|
|
427
770
|
|
|
428
|
-
const request = this.
|
|
429
|
-
this.log.debug(`Simulating invalidate
|
|
771
|
+
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
772
|
+
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
773
|
+
|
|
774
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
430
775
|
|
|
431
776
|
try {
|
|
432
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
433
|
-
|
|
777
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
778
|
+
request,
|
|
779
|
+
undefined,
|
|
780
|
+
undefined,
|
|
781
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
782
|
+
);
|
|
783
|
+
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
784
|
+
...logData,
|
|
785
|
+
request,
|
|
786
|
+
gasUsed,
|
|
787
|
+
});
|
|
434
788
|
|
|
435
|
-
return {
|
|
789
|
+
return {
|
|
790
|
+
request,
|
|
791
|
+
gasUsed,
|
|
792
|
+
checkpointNumber,
|
|
793
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
794
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
795
|
+
reason,
|
|
796
|
+
};
|
|
436
797
|
} catch (err) {
|
|
437
798
|
const viemError = formatViemError(err);
|
|
438
799
|
|
|
439
|
-
// If the error is due to the
|
|
440
|
-
// we can safely ignore it and return undefined so we go ahead with
|
|
441
|
-
if (viemError.message?.includes('
|
|
800
|
+
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
801
|
+
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
802
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
442
803
|
this.log.verbose(
|
|
443
|
-
`Simulation for invalidate
|
|
804
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
444
805
|
{ ...logData, request, error: viemError.message },
|
|
445
806
|
);
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
this.log.verbose(`
|
|
807
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
808
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
809
|
+
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
449
810
|
return undefined;
|
|
450
811
|
} else {
|
|
451
812
|
this.log.error(
|
|
452
|
-
`Simulation for invalidate ${
|
|
813
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
453
814
|
viemError,
|
|
454
815
|
logData,
|
|
455
816
|
);
|
|
456
|
-
throw new Error(
|
|
457
|
-
|
|
458
|
-
|
|
817
|
+
throw new Error(
|
|
818
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
819
|
+
{
|
|
820
|
+
cause: viemError,
|
|
821
|
+
},
|
|
822
|
+
);
|
|
459
823
|
}
|
|
460
824
|
}
|
|
461
825
|
|
|
462
|
-
// Otherwise, throw. We cannot build the next
|
|
463
|
-
this.log.error(`Simulation for invalidate
|
|
464
|
-
|
|
826
|
+
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
827
|
+
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
828
|
+
this.backupFailedTx({
|
|
829
|
+
id: keccak256(request.data!),
|
|
830
|
+
failureType: 'simulation',
|
|
831
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
832
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
833
|
+
error: { message: viemError.message, name: viemError.name },
|
|
834
|
+
context: {
|
|
835
|
+
actions: [`invalidate-${reason}`],
|
|
836
|
+
checkpointNumber,
|
|
837
|
+
sender: this.getSenderAddress().toString(),
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
465
841
|
}
|
|
466
842
|
}
|
|
467
843
|
|
|
468
|
-
private
|
|
844
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
469
845
|
if (validationResult.valid) {
|
|
470
|
-
throw new Error('Cannot invalidate a valid
|
|
846
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
471
847
|
}
|
|
472
848
|
|
|
473
|
-
const {
|
|
474
|
-
const logData = { ...
|
|
475
|
-
this.log.debug(`
|
|
849
|
+
const { checkpoint, committee, reason } = validationResult;
|
|
850
|
+
const logData = { ...checkpoint, reason };
|
|
851
|
+
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
476
852
|
|
|
477
|
-
const attestationsAndSigners =
|
|
478
|
-
validationResult.attestations,
|
|
479
|
-
).getPackedAttestations();
|
|
853
|
+
const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
|
|
480
854
|
|
|
481
855
|
if (reason === 'invalid-attestation') {
|
|
482
856
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
483
|
-
|
|
857
|
+
checkpoint.checkpointNumber,
|
|
484
858
|
attestationsAndSigners,
|
|
485
859
|
committee,
|
|
486
860
|
validationResult.invalidIndex,
|
|
487
861
|
);
|
|
488
862
|
} else if (reason === 'insufficient-attestations') {
|
|
489
863
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
490
|
-
|
|
864
|
+
checkpoint.checkpointNumber,
|
|
491
865
|
attestationsAndSigners,
|
|
492
866
|
committee,
|
|
493
867
|
);
|
|
@@ -497,47 +871,24 @@ export class SequencerPublisher {
|
|
|
497
871
|
}
|
|
498
872
|
}
|
|
499
873
|
|
|
500
|
-
/**
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
*
|
|
505
|
-
* @param block - The block to propose
|
|
506
|
-
* @param attestationData - The block's attestation data
|
|
507
|
-
*
|
|
508
|
-
*/
|
|
509
|
-
public async validateBlockForSubmission(
|
|
510
|
-
block: L2Block,
|
|
874
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
875
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
876
|
+
public async validateCheckpointForSubmission(
|
|
877
|
+
checkpoint: Checkpoint,
|
|
511
878
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
512
879
|
attestationsAndSignersSignature: Signature,
|
|
513
|
-
|
|
514
|
-
): Promise<
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
518
|
-
// so that the committee is recalculated correctly
|
|
519
|
-
const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
520
|
-
if (ignoreSignatures) {
|
|
521
|
-
const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
522
|
-
if (!committee) {
|
|
523
|
-
this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
524
|
-
throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
525
|
-
}
|
|
526
|
-
attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
527
|
-
CommitteeAttestation.fromAddress(committeeMember),
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const blobFields = block.getCheckpointBlobFields();
|
|
532
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
880
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
881
|
+
): Promise<void> {
|
|
882
|
+
const blobFields = checkpoint.toBlobFields();
|
|
883
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
533
884
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
534
885
|
|
|
535
886
|
const args = [
|
|
536
887
|
{
|
|
537
|
-
header:
|
|
538
|
-
archive: toHex(
|
|
888
|
+
header: checkpoint.header.toViem(),
|
|
889
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
539
890
|
oracleInput: {
|
|
540
|
-
feeAssetPriceModifier:
|
|
891
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
541
892
|
},
|
|
542
893
|
},
|
|
543
894
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -546,13 +897,11 @@ export class SequencerPublisher {
|
|
|
546
897
|
blobInput,
|
|
547
898
|
] as const;
|
|
548
899
|
|
|
549
|
-
await this.simulateProposeTx(args,
|
|
550
|
-
return ts;
|
|
900
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
551
901
|
}
|
|
552
902
|
|
|
553
903
|
private async enqueueCastSignalHelper(
|
|
554
904
|
slotNumber: SlotNumber,
|
|
555
|
-
timestamp: bigint,
|
|
556
905
|
signalType: GovernanceSignalAction,
|
|
557
906
|
payload: EthAddress,
|
|
558
907
|
base: IEmpireBase,
|
|
@@ -573,10 +922,45 @@ export class SequencerPublisher {
|
|
|
573
922
|
const round = await base.computeRound(slotNumber);
|
|
574
923
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
575
924
|
|
|
925
|
+
if (roundInfo.quorumReached) {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
|
|
576
929
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
577
930
|
return false;
|
|
578
931
|
}
|
|
579
932
|
|
|
933
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
934
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Check if payload was already submitted to governance
|
|
939
|
+
const cacheKey = payload.toString();
|
|
940
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
941
|
+
try {
|
|
942
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
943
|
+
const proposed = await retry(
|
|
944
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
945
|
+
'Check if payload was proposed',
|
|
946
|
+
makeBackoff([0, 1, 2]),
|
|
947
|
+
this.log,
|
|
948
|
+
true,
|
|
949
|
+
);
|
|
950
|
+
if (proposed) {
|
|
951
|
+
this.payloadProposedCache.add(cacheKey);
|
|
952
|
+
}
|
|
953
|
+
} catch (err) {
|
|
954
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
960
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
|
|
580
964
|
const cachedLastVote = this.lastActions[signalType];
|
|
581
965
|
this.lastActions[signalType] = slotNumber;
|
|
582
966
|
const action = signalType;
|
|
@@ -595,11 +979,30 @@ export class SequencerPublisher {
|
|
|
595
979
|
lastValidL2Slot: slotNumber,
|
|
596
980
|
});
|
|
597
981
|
|
|
982
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
983
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
984
|
+
|
|
598
985
|
try {
|
|
599
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
986
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
600
987
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
601
988
|
} catch (err) {
|
|
602
|
-
|
|
989
|
+
const viemError = formatViemError(err);
|
|
990
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
991
|
+
simulationTimestamp: timestamp,
|
|
992
|
+
l1BlockNumber,
|
|
993
|
+
});
|
|
994
|
+
this.backupFailedTx({
|
|
995
|
+
id: keccak256(request.data!),
|
|
996
|
+
failureType: 'simulation',
|
|
997
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
998
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
999
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1000
|
+
context: {
|
|
1001
|
+
actions: [action],
|
|
1002
|
+
slot: slotNumber,
|
|
1003
|
+
sender: this.getSenderAddress().toString(),
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
603
1006
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
604
1007
|
}
|
|
605
1008
|
|
|
@@ -619,14 +1022,14 @@ export class SequencerPublisher {
|
|
|
619
1022
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
620
1023
|
if (!success) {
|
|
621
1024
|
this.log.error(
|
|
622
|
-
`Signaling in
|
|
1025
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
623
1026
|
logData,
|
|
624
1027
|
);
|
|
625
1028
|
this.lastActions[signalType] = cachedLastVote;
|
|
626
1029
|
return false;
|
|
627
1030
|
} else {
|
|
628
1031
|
this.log.info(
|
|
629
|
-
`Signaling in
|
|
1032
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
630
1033
|
logData,
|
|
631
1034
|
);
|
|
632
1035
|
return true;
|
|
@@ -636,22 +1039,30 @@ export class SequencerPublisher {
|
|
|
636
1039
|
return true;
|
|
637
1040
|
}
|
|
638
1041
|
|
|
1042
|
+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
1043
|
+
const key = payload.toString();
|
|
1044
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
1045
|
+
if (cached) {
|
|
1046
|
+
return cached;
|
|
1047
|
+
}
|
|
1048
|
+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
1049
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
1050
|
+
return isEmpty;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
639
1053
|
/**
|
|
640
1054
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
641
1055
|
* @param slotNumber - The slot number to cast a signal for.
|
|
642
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
643
1056
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
644
1057
|
*/
|
|
645
1058
|
public enqueueGovernanceCastSignal(
|
|
646
1059
|
governancePayload: EthAddress,
|
|
647
1060
|
slotNumber: SlotNumber,
|
|
648
|
-
timestamp: bigint,
|
|
649
1061
|
signerAddress: EthAddress,
|
|
650
1062
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
651
1063
|
): Promise<boolean> {
|
|
652
1064
|
return this.enqueueCastSignalHelper(
|
|
653
1065
|
slotNumber,
|
|
654
|
-
timestamp,
|
|
655
1066
|
'governance-signal',
|
|
656
1067
|
governancePayload,
|
|
657
1068
|
this.govProposerContract,
|
|
@@ -664,7 +1075,6 @@ export class SequencerPublisher {
|
|
|
664
1075
|
public async enqueueSlashingActions(
|
|
665
1076
|
actions: ProposerSlashAction[],
|
|
666
1077
|
slotNumber: SlotNumber,
|
|
667
|
-
timestamp: bigint,
|
|
668
1078
|
signerAddress: EthAddress,
|
|
669
1079
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
670
1080
|
): Promise<boolean> {
|
|
@@ -675,58 +1085,6 @@ export class SequencerPublisher {
|
|
|
675
1085
|
|
|
676
1086
|
for (const action of actions) {
|
|
677
1087
|
switch (action.type) {
|
|
678
|
-
case 'vote-empire-payload': {
|
|
679
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
680
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
681
|
-
break;
|
|
682
|
-
}
|
|
683
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
684
|
-
signerAddress,
|
|
685
|
-
});
|
|
686
|
-
await this.enqueueCastSignalHelper(
|
|
687
|
-
slotNumber,
|
|
688
|
-
timestamp,
|
|
689
|
-
'empire-slashing-signal',
|
|
690
|
-
action.payload,
|
|
691
|
-
this.slashingProposerContract,
|
|
692
|
-
signerAddress,
|
|
693
|
-
signer,
|
|
694
|
-
);
|
|
695
|
-
break;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
case 'create-empire-payload': {
|
|
699
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
700
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
701
|
-
await this.simulateAndEnqueueRequest(
|
|
702
|
-
'create-empire-payload',
|
|
703
|
-
request,
|
|
704
|
-
(receipt: TransactionReceipt) =>
|
|
705
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
706
|
-
slotNumber,
|
|
707
|
-
timestamp,
|
|
708
|
-
);
|
|
709
|
-
break;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
case 'execute-empire-payload': {
|
|
713
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
714
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
715
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
719
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
720
|
-
await this.simulateAndEnqueueRequest(
|
|
721
|
-
'execute-empire-payload',
|
|
722
|
-
request,
|
|
723
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
724
|
-
slotNumber,
|
|
725
|
-
timestamp,
|
|
726
|
-
);
|
|
727
|
-
break;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
1088
|
case 'vote-offenses': {
|
|
731
1089
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
732
1090
|
slotNumber,
|
|
@@ -734,19 +1092,17 @@ export class SequencerPublisher {
|
|
|
734
1092
|
votesCount: action.votes.length,
|
|
735
1093
|
signerAddress,
|
|
736
1094
|
});
|
|
737
|
-
if (this.slashingProposerContract
|
|
738
|
-
this.log.error('
|
|
1095
|
+
if (!this.slashingProposerContract) {
|
|
1096
|
+
this.log.error('No slashing proposer contract available');
|
|
739
1097
|
return false;
|
|
740
1098
|
}
|
|
741
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
742
1099
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
743
|
-
const request = await
|
|
1100
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
744
1101
|
await this.simulateAndEnqueueRequest(
|
|
745
1102
|
'vote-offenses',
|
|
746
1103
|
request,
|
|
747
|
-
(receipt: TransactionReceipt) => !!
|
|
1104
|
+
(receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
|
|
748
1105
|
slotNumber,
|
|
749
|
-
timestamp,
|
|
750
1106
|
);
|
|
751
1107
|
break;
|
|
752
1108
|
}
|
|
@@ -757,18 +1113,20 @@ export class SequencerPublisher {
|
|
|
757
1113
|
round: action.round,
|
|
758
1114
|
signerAddress,
|
|
759
1115
|
});
|
|
760
|
-
if (this.slashingProposerContract
|
|
761
|
-
this.log.error('
|
|
1116
|
+
if (!this.slashingProposerContract) {
|
|
1117
|
+
this.log.error('No slashing proposer contract available');
|
|
762
1118
|
return false;
|
|
763
1119
|
}
|
|
764
|
-
const
|
|
765
|
-
|
|
1120
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1121
|
+
action.round,
|
|
1122
|
+
action.committees,
|
|
1123
|
+
);
|
|
766
1124
|
await this.simulateAndEnqueueRequest(
|
|
767
1125
|
'execute-slash',
|
|
768
|
-
|
|
769
|
-
(receipt: TransactionReceipt) =>
|
|
1126
|
+
executeRequest,
|
|
1127
|
+
(receipt: TransactionReceipt) =>
|
|
1128
|
+
!!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
|
|
770
1129
|
slotNumber,
|
|
771
|
-
timestamp,
|
|
772
1130
|
);
|
|
773
1131
|
break;
|
|
774
1132
|
}
|
|
@@ -783,33 +1141,30 @@ export class SequencerPublisher {
|
|
|
783
1141
|
return true;
|
|
784
1142
|
}
|
|
785
1143
|
|
|
786
|
-
/**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
* @param block - L2 block to propose.
|
|
790
|
-
* @returns True if the tx has been enqueued, throws otherwise. See #9315
|
|
791
|
-
*/
|
|
792
|
-
public async enqueueProposeL2Block(
|
|
793
|
-
block: L2Block,
|
|
1144
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
1145
|
+
public async enqueueProposeCheckpoint(
|
|
1146
|
+
checkpoint: Checkpoint,
|
|
794
1147
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
795
1148
|
attestationsAndSignersSignature: Signature,
|
|
796
|
-
opts:
|
|
797
|
-
): Promise<
|
|
798
|
-
const checkpointHeader =
|
|
1149
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1150
|
+
): Promise<void> {
|
|
1151
|
+
const checkpointHeader = checkpoint.header;
|
|
799
1152
|
|
|
800
|
-
const blobFields =
|
|
801
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1153
|
+
const blobFields = checkpoint.toBlobFields();
|
|
1154
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
802
1155
|
|
|
803
|
-
const proposeTxArgs = {
|
|
1156
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
804
1157
|
header: checkpointHeader,
|
|
805
|
-
archive:
|
|
806
|
-
body: block.body.toBuffer(),
|
|
1158
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
807
1159
|
blobs,
|
|
808
1160
|
attestationsAndSigners,
|
|
809
1161
|
attestationsAndSignersSignature,
|
|
1162
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
810
1163
|
};
|
|
811
1164
|
|
|
812
|
-
|
|
1165
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
|
|
1166
|
+
.withoutBlobCheck()
|
|
1167
|
+
.build();
|
|
813
1168
|
|
|
814
1169
|
try {
|
|
815
1170
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
@@ -817,22 +1172,53 @@ export class SequencerPublisher {
|
|
|
817
1172
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
818
1173
|
// make time consistency checks break.
|
|
819
1174
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
820
|
-
|
|
1175
|
+
await this.validateCheckpointForSubmission(
|
|
1176
|
+
checkpoint,
|
|
1177
|
+
attestationsAndSigners,
|
|
1178
|
+
attestationsAndSignersSignature,
|
|
1179
|
+
simulationOverridesPlan,
|
|
1180
|
+
);
|
|
821
1181
|
} catch (err: any) {
|
|
822
|
-
this.log.error(`
|
|
823
|
-
...
|
|
824
|
-
slotNumber:
|
|
825
|
-
|
|
1182
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1183
|
+
...checkpoint.getStats(),
|
|
1184
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
1185
|
+
simulationOverridesPlan,
|
|
826
1186
|
});
|
|
827
1187
|
throw err;
|
|
828
1188
|
}
|
|
829
1189
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1190
|
+
// Build a pre-check callback that re-validates the checkpoint before L1 submission.
|
|
1191
|
+
// During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
|
|
1192
|
+
let preCheck = undefined;
|
|
1193
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
1194
|
+
preCheck = async () => {
|
|
1195
|
+
this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
|
|
1196
|
+
await this.validateCheckpointForSubmission(
|
|
1197
|
+
checkpoint,
|
|
1198
|
+
attestationsAndSigners,
|
|
1199
|
+
attestationsAndSignersSignature,
|
|
1200
|
+
simulationOverridesPlan,
|
|
1201
|
+
);
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1206
|
+
...checkpoint.toCheckpointInfo(),
|
|
1207
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1208
|
+
simulationOverridesPlan,
|
|
1209
|
+
});
|
|
1210
|
+
await this.addProposeTx(
|
|
1211
|
+
checkpoint,
|
|
1212
|
+
proposeTxArgs,
|
|
1213
|
+
{ txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
|
|
1214
|
+
preCheck,
|
|
1215
|
+
);
|
|
833
1216
|
}
|
|
834
1217
|
|
|
835
|
-
public
|
|
1218
|
+
public enqueueInvalidateCheckpoint(
|
|
1219
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
1220
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
1221
|
+
) {
|
|
836
1222
|
if (!request) {
|
|
837
1223
|
return;
|
|
838
1224
|
}
|
|
@@ -840,9 +1226,9 @@ export class SequencerPublisher {
|
|
|
840
1226
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
841
1227
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
842
1228
|
|
|
843
|
-
const { gasUsed,
|
|
844
|
-
const logData = { gasUsed,
|
|
845
|
-
this.log.verbose(`Enqueuing invalidate
|
|
1229
|
+
const { gasUsed, checkpointNumber } = request;
|
|
1230
|
+
const logData = { gasUsed, checkpointNumber, gasLimit, opts };
|
|
1231
|
+
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
846
1232
|
this.addRequest({
|
|
847
1233
|
action: `invalidate-by-${request.reason}`,
|
|
848
1234
|
request: request.request,
|
|
@@ -855,9 +1241,9 @@ export class SequencerPublisher {
|
|
|
855
1241
|
result.receipt.status === 'success' &&
|
|
856
1242
|
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
857
1243
|
if (!success) {
|
|
858
|
-
this.log.warn(`Invalidate
|
|
1244
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
859
1245
|
} else {
|
|
860
|
-
this.log.info(`Invalidate
|
|
1246
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
861
1247
|
}
|
|
862
1248
|
return !!success;
|
|
863
1249
|
},
|
|
@@ -869,8 +1255,8 @@ export class SequencerPublisher {
|
|
|
869
1255
|
request: L1TxRequest,
|
|
870
1256
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
871
1257
|
slotNumber: SlotNumber,
|
|
872
|
-
timestamp: bigint,
|
|
873
1258
|
) {
|
|
1259
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
874
1260
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
875
1261
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
876
1262
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
@@ -882,13 +1268,31 @@ export class SequencerPublisher {
|
|
|
882
1268
|
|
|
883
1269
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
884
1270
|
|
|
1271
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1272
|
+
|
|
885
1273
|
let gasUsed: bigint;
|
|
1274
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1275
|
+
|
|
886
1276
|
try {
|
|
887
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1277
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
888
1278
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
889
1279
|
} catch (err) {
|
|
890
|
-
const viemError = formatViemError(err);
|
|
1280
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
891
1281
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1282
|
+
|
|
1283
|
+
this.backupFailedTx({
|
|
1284
|
+
id: keccak256(request.data!),
|
|
1285
|
+
failureType: 'simulation',
|
|
1286
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1287
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1288
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1289
|
+
context: {
|
|
1290
|
+
actions: [action],
|
|
1291
|
+
slot: slotNumber,
|
|
1292
|
+
sender: this.getSenderAddress().toString(),
|
|
1293
|
+
},
|
|
1294
|
+
});
|
|
1295
|
+
|
|
892
1296
|
return false;
|
|
893
1297
|
}
|
|
894
1298
|
|
|
@@ -896,10 +1300,14 @@ export class SequencerPublisher {
|
|
|
896
1300
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
897
1301
|
logData.gasLimit = gasLimit;
|
|
898
1302
|
|
|
1303
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1304
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1305
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1306
|
+
|
|
899
1307
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
900
1308
|
this.addRequest({
|
|
901
1309
|
action,
|
|
902
|
-
request,
|
|
1310
|
+
request: requestWithAbi,
|
|
903
1311
|
gasConfig: { gasLimit },
|
|
904
1312
|
lastValidL2Slot: slotNumber,
|
|
905
1313
|
checkSuccess: (_req, result) => {
|
|
@@ -924,6 +1332,7 @@ export class SequencerPublisher {
|
|
|
924
1332
|
*/
|
|
925
1333
|
public interrupt() {
|
|
926
1334
|
this.interrupted = true;
|
|
1335
|
+
this.interruptibleSleep.interrupt();
|
|
927
1336
|
this.l1TxUtils.interrupt();
|
|
928
1337
|
}
|
|
929
1338
|
|
|
@@ -933,11 +1342,7 @@ export class SequencerPublisher {
|
|
|
933
1342
|
this.l1TxUtils.restart();
|
|
934
1343
|
}
|
|
935
1344
|
|
|
936
|
-
private async prepareProposeTx(
|
|
937
|
-
encodedData: L1ProcessArgs,
|
|
938
|
-
timestamp: bigint,
|
|
939
|
-
options: { forcePendingBlockNumber?: number },
|
|
940
|
-
) {
|
|
1345
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
941
1346
|
const kzg = Blob.getViemKzgInstance();
|
|
942
1347
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
943
1348
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -968,9 +1373,27 @@ export class SequencerPublisher {
|
|
|
968
1373
|
kzg,
|
|
969
1374
|
},
|
|
970
1375
|
)
|
|
971
|
-
.catch(err => {
|
|
972
|
-
const
|
|
973
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1376
|
+
.catch(async err => {
|
|
1377
|
+
const viemError = formatViemError(err);
|
|
1378
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1379
|
+
const validateBlobsData = encodeFunctionData({
|
|
1380
|
+
abi: RollupAbi,
|
|
1381
|
+
functionName: 'validateBlobs',
|
|
1382
|
+
args: [blobInput],
|
|
1383
|
+
});
|
|
1384
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1385
|
+
this.backupFailedTx({
|
|
1386
|
+
id: keccak256(validateBlobsData),
|
|
1387
|
+
failureType: 'simulation',
|
|
1388
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1389
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1390
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1391
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1392
|
+
context: {
|
|
1393
|
+
actions: ['validate-blobs'],
|
|
1394
|
+
sender: this.getSenderAddress().toString(),
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
974
1397
|
throw new Error('Failed to validate blobs');
|
|
975
1398
|
});
|
|
976
1399
|
}
|
|
@@ -981,8 +1404,7 @@ export class SequencerPublisher {
|
|
|
981
1404
|
header: encodedData.header.toViem(),
|
|
982
1405
|
archive: toHex(encodedData.archive),
|
|
983
1406
|
oracleInput: {
|
|
984
|
-
|
|
985
|
-
feeAssetPriceModifier: 0n,
|
|
1407
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
986
1408
|
},
|
|
987
1409
|
},
|
|
988
1410
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -991,7 +1413,7 @@ export class SequencerPublisher {
|
|
|
991
1413
|
blobInput,
|
|
992
1414
|
] as const;
|
|
993
1415
|
|
|
994
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1416
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
995
1417
|
|
|
996
1418
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
997
1419
|
}
|
|
@@ -999,7 +1421,6 @@ export class SequencerPublisher {
|
|
|
999
1421
|
/**
|
|
1000
1422
|
* Simulates the propose tx with eth_simulateV1
|
|
1001
1423
|
* @param args - The propose tx args
|
|
1002
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1003
1424
|
* @returns The simulation result
|
|
1004
1425
|
*/
|
|
1005
1426
|
private async simulateProposeTx(
|
|
@@ -1008,7 +1429,7 @@ export class SequencerPublisher {
|
|
|
1008
1429
|
readonly header: ViemHeader;
|
|
1009
1430
|
readonly archive: `0x${string}`;
|
|
1010
1431
|
readonly oracleInput: {
|
|
1011
|
-
readonly feeAssetPriceModifier:
|
|
1432
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1012
1433
|
};
|
|
1013
1434
|
},
|
|
1014
1435
|
ViemCommitteeAttestations,
|
|
@@ -1016,8 +1437,7 @@ export class SequencerPublisher {
|
|
|
1016
1437
|
ViemSignature,
|
|
1017
1438
|
`0x${string}`,
|
|
1018
1439
|
],
|
|
1019
|
-
|
|
1020
|
-
options: { forcePendingBlockNumber?: number },
|
|
1440
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
1021
1441
|
) {
|
|
1022
1442
|
const rollupData = encodeFunctionData({
|
|
1023
1443
|
abi: RollupAbi,
|
|
@@ -1025,23 +1445,7 @@ export class SequencerPublisher {
|
|
|
1025
1445
|
args,
|
|
1026
1446
|
});
|
|
1027
1447
|
|
|
1028
|
-
|
|
1029
|
-
const forcePendingBlockNumberStateDiff = (
|
|
1030
|
-
options.forcePendingBlockNumber !== undefined
|
|
1031
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
|
|
1032
|
-
: []
|
|
1033
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
1034
|
-
|
|
1035
|
-
const stateOverrides: StateOverride = [
|
|
1036
|
-
{
|
|
1037
|
-
address: this.rollupContract.address,
|
|
1038
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1039
|
-
stateDiff: [
|
|
1040
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1041
|
-
...forcePendingBlockNumberStateDiff,
|
|
1042
|
-
],
|
|
1043
|
-
},
|
|
1044
|
-
];
|
|
1448
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1045
1449
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1046
1450
|
if (this.proposerAddressForSimulation) {
|
|
1047
1451
|
stateOverrides.push({
|
|
@@ -1050,25 +1454,27 @@ export class SequencerPublisher {
|
|
|
1050
1454
|
});
|
|
1051
1455
|
}
|
|
1052
1456
|
|
|
1457
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1458
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1459
|
+
|
|
1053
1460
|
const simulationResult = await this.l1TxUtils
|
|
1054
1461
|
.simulate(
|
|
1055
1462
|
{
|
|
1056
1463
|
to: this.rollupContract.address,
|
|
1057
1464
|
data: rollupData,
|
|
1058
|
-
gas:
|
|
1465
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1059
1466
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1060
1467
|
},
|
|
1061
1468
|
{
|
|
1062
|
-
|
|
1063
|
-
time: timestamp + 1n,
|
|
1469
|
+
time: simTs,
|
|
1064
1470
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1065
|
-
gasLimit:
|
|
1471
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1066
1472
|
},
|
|
1067
1473
|
stateOverrides,
|
|
1068
1474
|
RollupAbi,
|
|
1069
1475
|
{
|
|
1070
1476
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1071
|
-
fallbackGasEstimate:
|
|
1477
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1072
1478
|
},
|
|
1073
1479
|
)
|
|
1074
1480
|
.catch(err => {
|
|
@@ -1078,11 +1484,23 @@ export class SequencerPublisher {
|
|
|
1078
1484
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1079
1485
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1080
1486
|
return {
|
|
1081
|
-
gasUsed:
|
|
1487
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1082
1488
|
logs: [],
|
|
1083
1489
|
};
|
|
1084
1490
|
}
|
|
1085
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1491
|
+
this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
|
|
1492
|
+
this.backupFailedTx({
|
|
1493
|
+
id: keccak256(rollupData),
|
|
1494
|
+
failureType: 'simulation',
|
|
1495
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1496
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1497
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1498
|
+
context: {
|
|
1499
|
+
actions: ['propose'],
|
|
1500
|
+
slot: Number(args[0].header.slotNumber),
|
|
1501
|
+
sender: this.getSenderAddress().toString(),
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1086
1504
|
throw err;
|
|
1087
1505
|
});
|
|
1088
1506
|
|
|
@@ -1090,17 +1508,17 @@ export class SequencerPublisher {
|
|
|
1090
1508
|
}
|
|
1091
1509
|
|
|
1092
1510
|
private async addProposeTx(
|
|
1093
|
-
|
|
1511
|
+
checkpoint: Checkpoint,
|
|
1094
1512
|
encodedData: L1ProcessArgs,
|
|
1095
|
-
opts:
|
|
1096
|
-
|
|
1513
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1514
|
+
preCheck?: () => Promise<void>,
|
|
1097
1515
|
): Promise<void> {
|
|
1516
|
+
const slot = checkpoint.header.slotNumber;
|
|
1098
1517
|
const timer = new Timer();
|
|
1099
1518
|
const kzg = Blob.getViemKzgInstance();
|
|
1100
1519
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1101
1520
|
encodedData,
|
|
1102
|
-
|
|
1103
|
-
opts,
|
|
1521
|
+
opts.simulationOverridesPlan,
|
|
1104
1522
|
);
|
|
1105
1523
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1106
1524
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
@@ -1109,11 +1527,13 @@ export class SequencerPublisher {
|
|
|
1109
1527
|
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1110
1528
|
);
|
|
1111
1529
|
|
|
1112
|
-
// Send the blobs to the blob
|
|
1113
|
-
// tx fails but it does get mined. We make sure that the blobs are sent to the blob
|
|
1114
|
-
void
|
|
1115
|
-
this.
|
|
1116
|
-
|
|
1530
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1531
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
1532
|
+
void Promise.resolve().then(() =>
|
|
1533
|
+
this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
|
|
1534
|
+
this.log.error('Failed to send blobs to blob client');
|
|
1535
|
+
}),
|
|
1536
|
+
);
|
|
1117
1537
|
|
|
1118
1538
|
return this.addRequest({
|
|
1119
1539
|
action: 'propose',
|
|
@@ -1121,8 +1541,9 @@ export class SequencerPublisher {
|
|
|
1121
1541
|
to: this.rollupContract.address,
|
|
1122
1542
|
data: rollupData,
|
|
1123
1543
|
},
|
|
1124
|
-
lastValidL2Slot:
|
|
1125
|
-
gasConfig: {
|
|
1544
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1545
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
|
|
1546
|
+
preCheck,
|
|
1126
1547
|
blobConfig: {
|
|
1127
1548
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1128
1549
|
kzg,
|
|
@@ -1136,11 +1557,12 @@ export class SequencerPublisher {
|
|
|
1136
1557
|
receipt &&
|
|
1137
1558
|
receipt.status === 'success' &&
|
|
1138
1559
|
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1560
|
+
|
|
1139
1561
|
if (success) {
|
|
1140
1562
|
const endBlock = receipt.blockNumber;
|
|
1141
1563
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1142
1564
|
const { calldataGas, calldataSize, sender } = stats!;
|
|
1143
|
-
const publishStats:
|
|
1565
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1144
1566
|
gasPrice: receipt.effectiveGasPrice,
|
|
1145
1567
|
gasUsed: receipt.gasUsed,
|
|
1146
1568
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
@@ -1149,26 +1571,42 @@ export class SequencerPublisher {
|
|
|
1149
1571
|
calldataGas,
|
|
1150
1572
|
calldataSize,
|
|
1151
1573
|
sender,
|
|
1152
|
-
...
|
|
1574
|
+
...checkpoint.getStats(),
|
|
1153
1575
|
eventName: 'rollup-published-to-l1',
|
|
1154
1576
|
blobCount: encodedData.blobs.length,
|
|
1155
1577
|
inclusionBlocks,
|
|
1156
1578
|
};
|
|
1157
|
-
this.log.info(`Published
|
|
1579
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
1580
|
+
...stats,
|
|
1581
|
+
...checkpoint.getStats(),
|
|
1582
|
+
...pick(receipt, 'transactionHash', 'blockHash'),
|
|
1583
|
+
});
|
|
1158
1584
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1159
1585
|
|
|
1160
1586
|
return true;
|
|
1161
1587
|
} else {
|
|
1162
1588
|
this.metrics.recordFailedTx('process');
|
|
1163
|
-
this.log.error(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
});
|
|
1589
|
+
this.log.error(
|
|
1590
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1591
|
+
undefined,
|
|
1592
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1593
|
+
);
|
|
1169
1594
|
return false;
|
|
1170
1595
|
}
|
|
1171
1596
|
},
|
|
1172
1597
|
});
|
|
1173
1598
|
}
|
|
1599
|
+
|
|
1600
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1601
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
|
|
1602
|
+
private getSimulationTimestamp(slot: SlotNumber): bigint {
|
|
1603
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1604
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1608
|
+
private getNextL1SlotTimestamp(): bigint {
|
|
1609
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1610
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1611
|
+
}
|
|
1174
1612
|
}
|