@aztec/sequencer-client 0.0.1-commit.b655e406 → 0.0.1-commit.c0b82b2
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/index.d.ts +1 -1
- package/dest/client/sequencer-client.d.ts +21 -16
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +45 -26
- package/dest/config.d.ts +14 -8
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +90 -33
- package/dest/global_variable_builder/global_builder.d.ts +20 -16
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +52 -39
- package/dest/global_variable_builder/index.d.ts +1 -1
- 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 +43 -20
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +109 -34
- 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 +15 -6
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +14 -3
- 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 +95 -67
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +935 -182
- package/dest/sequencer/checkpoint_proposal_job.d.ts +102 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job.js +1219 -0
- package/dest/sequencer/checkpoint_voter.d.ts +35 -0
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_voter.js +109 -0
- package/dest/sequencer/config.d.ts +3 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +1 -1
- package/dest/sequencer/errors.d.ts.map +1 -1
- package/dest/sequencer/events.d.ts +46 -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 +44 -3
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +232 -50
- package/dest/sequencer/sequencer.d.ts +122 -144
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +736 -521
- package/dest/sequencer/timetable.d.ts +51 -14
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +145 -59
- 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 +97 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +222 -0
- package/dest/test/utils.d.ts +53 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +104 -0
- package/package.json +33 -30
- package/src/client/sequencer-client.ts +54 -47
- package/src/config.ts +103 -42
- package/src/global_variable_builder/global_builder.ts +67 -59
- package/src/index.ts +1 -7
- package/src/publisher/config.ts +139 -50
- 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 +30 -11
- package/src/publisher/sequencer-publisher-metrics.ts +19 -71
- package/src/publisher/sequencer-publisher.ts +633 -234
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/checkpoint_proposal_job.ts +926 -0
- package/src/sequencer/checkpoint_voter.ts +130 -0
- package/src/sequencer/config.ts +2 -1
- package/src/sequencer/events.ts +27 -0
- package/src/sequencer/index.ts +3 -1
- package/src/sequencer/metrics.ts +296 -61
- package/src/sequencer/sequencer.ts +488 -711
- package/src/sequencer/timetable.ts +175 -80
- 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 +320 -0
- package/src/test/utils.ts +167 -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 -130
- 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 -218
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -132
|
@@ -1,48 +1,63 @@
|
|
|
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
6
|
type EmpireSlashingProposerContract,
|
|
7
|
-
|
|
7
|
+
FeeAssetPriceOracle,
|
|
8
8
|
type GovernanceProposerContract,
|
|
9
9
|
type IEmpireBase,
|
|
10
|
-
type L1BlobInputs,
|
|
11
|
-
type L1ContractsConfig,
|
|
12
|
-
type L1TxConfig,
|
|
13
|
-
type L1TxRequest,
|
|
14
10
|
MULTI_CALL_3_ADDRESS,
|
|
15
11
|
Multicall3,
|
|
16
12
|
RollupContract,
|
|
17
13
|
type TallySlashingProposerContract,
|
|
18
|
-
type TransactionStats,
|
|
19
14
|
type ViemCommitteeAttestations,
|
|
20
15
|
type ViemHeader,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
} from '@aztec/ethereum/contracts';
|
|
17
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
18
|
+
import {
|
|
19
|
+
type L1BlobInputs,
|
|
20
|
+
type L1TxConfig,
|
|
21
|
+
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
23
|
+
MAX_L1_TX_LIMIT,
|
|
24
|
+
type TransactionStats,
|
|
25
|
+
WEI_CONST,
|
|
26
|
+
} from '@aztec/ethereum/l1-tx-utils';
|
|
27
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
28
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
29
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
30
|
+
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
31
|
+
import { pick } from '@aztec/foundation/collection';
|
|
32
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
28
33
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
29
34
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
30
|
-
import type { Fr } from '@aztec/foundation/fields';
|
|
31
35
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
36
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
32
37
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
33
38
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
34
39
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
35
40
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
36
|
-
import {
|
|
41
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
42
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
37
43
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
38
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
39
|
-
import type {
|
|
40
|
-
import {
|
|
41
|
-
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
45
|
+
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
46
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
42
47
|
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
import {
|
|
49
|
+
type Hex,
|
|
50
|
+
type StateOverride,
|
|
51
|
+
type TransactionReceipt,
|
|
52
|
+
type TypedDataDefinition,
|
|
53
|
+
encodeFunctionData,
|
|
54
|
+
keccak256,
|
|
55
|
+
multicall3Abi,
|
|
56
|
+
toHex,
|
|
57
|
+
} from 'viem';
|
|
58
|
+
|
|
59
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
60
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
46
61
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
47
62
|
|
|
48
63
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -51,14 +66,14 @@ type L1ProcessArgs = {
|
|
|
51
66
|
header: CheckpointHeader;
|
|
52
67
|
/** A root of the archive tree after the L2 block is applied. */
|
|
53
68
|
archive: Buffer;
|
|
54
|
-
/** State reference after the L2 block is applied. */
|
|
55
|
-
stateReference: StateReference;
|
|
56
69
|
/** L2 block blobs containing all tx effects. */
|
|
57
70
|
blobs: Blob[];
|
|
58
71
|
/** Attestations */
|
|
59
72
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
60
73
|
/** Attestations and signers signature */
|
|
61
74
|
attestationsAndSignersSignature: Signature;
|
|
75
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
76
|
+
feeAssetPriceModifier: bigint;
|
|
62
77
|
};
|
|
63
78
|
|
|
64
79
|
export const Actions = [
|
|
@@ -80,18 +95,18 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
|
|
|
80
95
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
81
96
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
82
97
|
|
|
83
|
-
export type
|
|
98
|
+
export type InvalidateCheckpointRequest = {
|
|
84
99
|
request: L1TxRequest;
|
|
85
100
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
86
101
|
gasUsed: bigint;
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
checkpointNumber: CheckpointNumber;
|
|
103
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
89
104
|
};
|
|
90
105
|
|
|
91
106
|
interface RequestWithExpiry {
|
|
92
107
|
action: Action;
|
|
93
108
|
request: L1TxRequest;
|
|
94
|
-
lastValidL2Slot:
|
|
109
|
+
lastValidL2Slot: SlotNumber;
|
|
95
110
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
96
111
|
blobConfig?: L1BlobInputs;
|
|
97
112
|
checkSuccess: (
|
|
@@ -104,20 +119,29 @@ export class SequencerPublisher {
|
|
|
104
119
|
private interrupted = false;
|
|
105
120
|
private metrics: SequencerPublisherMetrics;
|
|
106
121
|
public epochCache: EpochCache;
|
|
122
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
107
123
|
|
|
108
124
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
109
125
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
110
126
|
|
|
111
|
-
protected lastActions: Partial<Record<Action,
|
|
127
|
+
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
128
|
+
|
|
129
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
130
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
112
131
|
|
|
113
132
|
protected log: Logger;
|
|
114
133
|
protected ethereumSlotDuration: bigint;
|
|
115
134
|
|
|
116
|
-
private
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
private blobClient: BlobClientInterface;
|
|
136
|
+
|
|
137
|
+
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
138
|
+
private proposerAddressForSimulation?: EthAddress;
|
|
139
|
+
|
|
140
|
+
/** L1 fee analyzer for fisherman mode */
|
|
141
|
+
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
142
|
+
|
|
143
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
144
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
121
145
|
|
|
122
146
|
// A CALL to a cold address is 2700 gas
|
|
123
147
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -125,20 +149,23 @@ export class SequencerPublisher {
|
|
|
125
149
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
126
150
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
127
151
|
|
|
128
|
-
public l1TxUtils:
|
|
152
|
+
public l1TxUtils: L1TxUtils;
|
|
129
153
|
public rollupContract: RollupContract;
|
|
130
154
|
public govProposerContract: GovernanceProposerContract;
|
|
131
155
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
132
156
|
public slashFactoryContract: SlashFactoryContract;
|
|
133
157
|
|
|
158
|
+
public readonly tracer: Tracer;
|
|
159
|
+
|
|
134
160
|
protected requests: RequestWithExpiry[] = [];
|
|
135
161
|
|
|
136
162
|
constructor(
|
|
137
|
-
private config:
|
|
163
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
164
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
138
165
|
deps: {
|
|
139
166
|
telemetry?: TelemetryClient;
|
|
140
|
-
|
|
141
|
-
l1TxUtils:
|
|
167
|
+
blobClient: BlobClientInterface;
|
|
168
|
+
l1TxUtils: L1TxUtils;
|
|
142
169
|
rollupContract: RollupContract;
|
|
143
170
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
144
171
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -146,7 +173,7 @@ export class SequencerPublisher {
|
|
|
146
173
|
epochCache: EpochCache;
|
|
147
174
|
dateProvider: DateProvider;
|
|
148
175
|
metrics: SequencerPublisherMetrics;
|
|
149
|
-
lastActions: Partial<Record<Action,
|
|
176
|
+
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
150
177
|
log?: Logger;
|
|
151
178
|
},
|
|
152
179
|
) {
|
|
@@ -155,11 +182,11 @@ export class SequencerPublisher {
|
|
|
155
182
|
this.epochCache = deps.epochCache;
|
|
156
183
|
this.lastActions = deps.lastActions;
|
|
157
184
|
|
|
158
|
-
this.
|
|
159
|
-
deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
|
|
185
|
+
this.blobClient = deps.blobClient;
|
|
160
186
|
|
|
161
187
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
162
188
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
189
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
163
190
|
this.l1TxUtils = deps.l1TxUtils;
|
|
164
191
|
|
|
165
192
|
this.rollupContract = deps.rollupContract;
|
|
@@ -173,24 +200,155 @@ export class SequencerPublisher {
|
|
|
173
200
|
this.slashingProposerContract = newSlashingProposer;
|
|
174
201
|
});
|
|
175
202
|
this.slashFactoryContract = deps.slashFactoryContract;
|
|
203
|
+
|
|
204
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
205
|
+
if (config.fishermanMode) {
|
|
206
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
207
|
+
this.l1TxUtils.client,
|
|
208
|
+
deps.dateProvider,
|
|
209
|
+
createLogger('sequencer:publisher:fee-analyzer'),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Initialize fee asset price oracle
|
|
214
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
215
|
+
this.l1TxUtils.client,
|
|
216
|
+
this.rollupContract,
|
|
217
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
221
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
226
|
+
* Does nothing if no store is configured.
|
|
227
|
+
*/
|
|
228
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
229
|
+
if (!this.failedTxStore) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tx: FailedL1Tx = {
|
|
234
|
+
...failedTx,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Fire and forget - don't block on backup
|
|
239
|
+
void this.failedTxStore
|
|
240
|
+
.then(store => store?.saveFailedTx(tx))
|
|
241
|
+
.catch(err => {
|
|
242
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
243
|
+
});
|
|
176
244
|
}
|
|
177
245
|
|
|
178
246
|
public getRollupContract(): RollupContract {
|
|
179
247
|
return this.rollupContract;
|
|
180
248
|
}
|
|
181
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Gets the fee asset price modifier from the oracle.
|
|
252
|
+
* Returns 0n if the oracle query fails.
|
|
253
|
+
*/
|
|
254
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
255
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
256
|
+
}
|
|
257
|
+
|
|
182
258
|
public getSenderAddress() {
|
|
183
259
|
return this.l1TxUtils.getSenderAddress();
|
|
184
260
|
}
|
|
185
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
264
|
+
*/
|
|
265
|
+
public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
|
|
266
|
+
return this.l1FeeAnalyzer;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Sets the proposer address to use for simulations in fisherman mode.
|
|
271
|
+
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
272
|
+
*/
|
|
273
|
+
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
274
|
+
this.proposerAddressForSimulation = proposerAddress;
|
|
275
|
+
}
|
|
276
|
+
|
|
186
277
|
public addRequest(request: RequestWithExpiry) {
|
|
187
278
|
this.requests.push(request);
|
|
188
279
|
}
|
|
189
280
|
|
|
190
|
-
public getCurrentL2Slot():
|
|
281
|
+
public getCurrentL2Slot(): SlotNumber {
|
|
191
282
|
return this.epochCache.getEpochAndSlotNow().slot;
|
|
192
283
|
}
|
|
193
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Clears all pending requests without sending them.
|
|
287
|
+
*/
|
|
288
|
+
public clearPendingRequests(): void {
|
|
289
|
+
const count = this.requests.length;
|
|
290
|
+
this.requests = [];
|
|
291
|
+
if (count > 0) {
|
|
292
|
+
this.log.debug(`Cleared ${count} pending request(s)`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
298
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
299
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
300
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
301
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
302
|
+
*/
|
|
303
|
+
public async analyzeL1Fees(
|
|
304
|
+
l2SlotNumber: SlotNumber,
|
|
305
|
+
onComplete?: (analysis: L1FeeAnalysisResult) => void,
|
|
306
|
+
): Promise<L1FeeAnalysisResult | undefined> {
|
|
307
|
+
if (!this.l1FeeAnalyzer) {
|
|
308
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const requestsToAnalyze = [...this.requests];
|
|
313
|
+
if (requestsToAnalyze.length === 0) {
|
|
314
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Extract blob config from requests (if any)
|
|
319
|
+
const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
320
|
+
const blobConfig = blobConfigs[0];
|
|
321
|
+
|
|
322
|
+
// Get gas configs
|
|
323
|
+
const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
324
|
+
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
325
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
|
|
326
|
+
|
|
327
|
+
// Get the transaction requests
|
|
328
|
+
const l1Requests = requestsToAnalyze.map(r => r.request);
|
|
329
|
+
|
|
330
|
+
// Start the analysis
|
|
331
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
332
|
+
l2SlotNumber,
|
|
333
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
334
|
+
l1Requests,
|
|
335
|
+
blobConfig,
|
|
336
|
+
onComplete,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
this.log.info('Started L1 fee analysis', {
|
|
340
|
+
analysisId,
|
|
341
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
342
|
+
requestCount: requestsToAnalyze.length,
|
|
343
|
+
hasBlobConfig: !!blobConfig,
|
|
344
|
+
gasLimit: gasLimit.toString(),
|
|
345
|
+
actions: requestsToAnalyze.map(r => r.action),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
349
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
350
|
+
}
|
|
351
|
+
|
|
194
352
|
/**
|
|
195
353
|
* Sends all requests that are still valid.
|
|
196
354
|
* @returns one of:
|
|
@@ -198,10 +356,11 @@ export class SequencerPublisher {
|
|
|
198
356
|
* - a receipt and errorMsg if it failed on L1
|
|
199
357
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
200
358
|
*/
|
|
359
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
201
360
|
public async sendRequests() {
|
|
202
361
|
const requestsToProcess = [...this.requests];
|
|
203
362
|
this.requests = [];
|
|
204
|
-
if (this.interrupted) {
|
|
363
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
205
364
|
return undefined;
|
|
206
365
|
}
|
|
207
366
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -244,7 +403,16 @@ export class SequencerPublisher {
|
|
|
244
403
|
|
|
245
404
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
246
405
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
247
|
-
|
|
406
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
407
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
408
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
409
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
410
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
411
|
+
requested: gasLimit,
|
|
412
|
+
capped: maxGas,
|
|
413
|
+
});
|
|
414
|
+
gasLimit = maxGas;
|
|
415
|
+
}
|
|
248
416
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
249
417
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
250
418
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -254,6 +422,21 @@ export class SequencerPublisher {
|
|
|
254
422
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
255
423
|
|
|
256
424
|
try {
|
|
425
|
+
// Capture context for failed tx backup before sending
|
|
426
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
427
|
+
const multicallData = encodeFunctionData({
|
|
428
|
+
abi: multicall3Abi,
|
|
429
|
+
functionName: 'aggregate3',
|
|
430
|
+
args: [
|
|
431
|
+
validRequests.map(r => ({
|
|
432
|
+
target: r.request.to!,
|
|
433
|
+
callData: r.request.data!,
|
|
434
|
+
allowFailure: true,
|
|
435
|
+
})),
|
|
436
|
+
],
|
|
437
|
+
});
|
|
438
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
439
|
+
|
|
257
440
|
this.log.debug('Forwarding transactions', {
|
|
258
441
|
validRequests: validRequests.map(request => request.action),
|
|
259
442
|
txConfig,
|
|
@@ -266,7 +449,12 @@ export class SequencerPublisher {
|
|
|
266
449
|
this.rollupContract.address,
|
|
267
450
|
this.log,
|
|
268
451
|
);
|
|
269
|
-
const
|
|
452
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
453
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
454
|
+
validRequests,
|
|
455
|
+
result,
|
|
456
|
+
txContext,
|
|
457
|
+
);
|
|
270
458
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
271
459
|
} catch (err) {
|
|
272
460
|
const viemError = formatViemError(err);
|
|
@@ -286,11 +474,25 @@ export class SequencerPublisher {
|
|
|
286
474
|
|
|
287
475
|
private callbackBundledTransactions(
|
|
288
476
|
requests: RequestWithExpiry[],
|
|
289
|
-
result
|
|
477
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
478
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
290
479
|
) {
|
|
291
480
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
292
481
|
if (result instanceof FormattedViemError) {
|
|
293
482
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
483
|
+
this.backupFailedTx({
|
|
484
|
+
id: keccak256(txContext.multicallData),
|
|
485
|
+
failureType: 'send-error',
|
|
486
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
487
|
+
blobData: txContext.blobData,
|
|
488
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
489
|
+
error: { message: result.message, name: result.name },
|
|
490
|
+
context: {
|
|
491
|
+
actions: requests.map(r => r.action),
|
|
492
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
493
|
+
sender: this.getSenderAddress().toString(),
|
|
494
|
+
},
|
|
495
|
+
});
|
|
294
496
|
return { failedActions: requests.map(r => r.action) };
|
|
295
497
|
} else {
|
|
296
498
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -303,6 +505,30 @@ export class SequencerPublisher {
|
|
|
303
505
|
failedActions.push(request.action);
|
|
304
506
|
}
|
|
305
507
|
}
|
|
508
|
+
// Single backup for the whole reverted tx
|
|
509
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
510
|
+
this.backupFailedTx({
|
|
511
|
+
id: result.receipt.transactionHash,
|
|
512
|
+
failureType: 'revert',
|
|
513
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
514
|
+
blobData: txContext.blobData,
|
|
515
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
516
|
+
receipt: {
|
|
517
|
+
transactionHash: result.receipt.transactionHash,
|
|
518
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
519
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
520
|
+
status: 'reverted',
|
|
521
|
+
},
|
|
522
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
523
|
+
context: {
|
|
524
|
+
actions: failedActions,
|
|
525
|
+
requests: requests
|
|
526
|
+
.filter(r => failedActions.includes(r.action))
|
|
527
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
528
|
+
sender: this.getSenderAddress().toString(),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
306
532
|
return { successfulActions, failedActions };
|
|
307
533
|
}
|
|
308
534
|
}
|
|
@@ -315,13 +541,15 @@ export class SequencerPublisher {
|
|
|
315
541
|
public canProposeAtNextEthBlock(
|
|
316
542
|
tipArchive: Fr,
|
|
317
543
|
msgSender: EthAddress,
|
|
318
|
-
opts: {
|
|
544
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
319
545
|
) {
|
|
320
546
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
321
547
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
322
548
|
|
|
323
549
|
return this.rollupContract
|
|
324
|
-
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration,
|
|
550
|
+
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
551
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
552
|
+
})
|
|
325
553
|
.catch(err => {
|
|
326
554
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
327
555
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -339,7 +567,11 @@ export class SequencerPublisher {
|
|
|
339
567
|
* It will throw if the block header is invalid.
|
|
340
568
|
* @param header - The block header to validate
|
|
341
569
|
*/
|
|
342
|
-
|
|
570
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
571
|
+
public async validateBlockHeader(
|
|
572
|
+
header: CheckpointHeader,
|
|
573
|
+
opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
|
|
574
|
+
): Promise<void> {
|
|
343
575
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
344
576
|
|
|
345
577
|
const args = [
|
|
@@ -348,15 +580,27 @@ export class SequencerPublisher {
|
|
|
348
580
|
[], // no signers
|
|
349
581
|
Signature.empty().toViemSignature(),
|
|
350
582
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
351
|
-
header.
|
|
583
|
+
header.blobsHash.toString(),
|
|
352
584
|
flags,
|
|
353
585
|
] as const;
|
|
354
586
|
|
|
355
587
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
588
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
589
|
+
opts?.forcePendingCheckpointNumber,
|
|
590
|
+
);
|
|
591
|
+
let balance = 0n;
|
|
592
|
+
if (this.config.fishermanMode) {
|
|
593
|
+
// In fisherman mode, we can't know where the proposer is publishing from
|
|
594
|
+
// so we just add sufficient balance to the multicall3 address
|
|
595
|
+
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
596
|
+
} else {
|
|
597
|
+
balance = await this.l1TxUtils.getSenderBalance();
|
|
598
|
+
}
|
|
599
|
+
stateOverrides.push({
|
|
600
|
+
address: MULTI_CALL_3_ADDRESS,
|
|
601
|
+
balance,
|
|
602
|
+
});
|
|
356
603
|
|
|
357
|
-
// use sender balance to simulate
|
|
358
|
-
const balance = await this.l1TxUtils.getSenderBalance();
|
|
359
|
-
this.log.debug(`Simulating validateHeader with balance: ${balance}`);
|
|
360
604
|
await this.l1TxUtils.simulate(
|
|
361
605
|
{
|
|
362
606
|
to: this.rollupContract.address,
|
|
@@ -364,86 +608,115 @@ export class SequencerPublisher {
|
|
|
364
608
|
from: MULTI_CALL_3_ADDRESS,
|
|
365
609
|
},
|
|
366
610
|
{ time: ts + 1n },
|
|
367
|
-
|
|
368
|
-
{ address: MULTI_CALL_3_ADDRESS, balance },
|
|
369
|
-
...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
|
|
370
|
-
],
|
|
611
|
+
stateOverrides,
|
|
371
612
|
);
|
|
372
613
|
this.log.debug(`Simulated validateHeader`);
|
|
373
614
|
}
|
|
374
615
|
|
|
375
616
|
/**
|
|
376
|
-
* Simulate making a call to invalidate a
|
|
377
|
-
* @param
|
|
617
|
+
* Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
|
|
618
|
+
* @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
|
|
378
619
|
*/
|
|
379
|
-
public async
|
|
380
|
-
validationResult:
|
|
381
|
-
): Promise<
|
|
620
|
+
public async simulateInvalidateCheckpoint(
|
|
621
|
+
validationResult: ValidateCheckpointResult,
|
|
622
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
382
623
|
if (validationResult.valid) {
|
|
383
624
|
return undefined;
|
|
384
625
|
}
|
|
385
626
|
|
|
386
|
-
const { reason,
|
|
387
|
-
const
|
|
388
|
-
const logData = { ...
|
|
627
|
+
const { reason, checkpoint } = validationResult;
|
|
628
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
629
|
+
const logData = { ...checkpoint, reason };
|
|
389
630
|
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
631
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
632
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
392
633
|
this.log.verbose(
|
|
393
|
-
`Skipping
|
|
394
|
-
{
|
|
634
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
635
|
+
{ currentCheckpointNumber, ...logData },
|
|
395
636
|
);
|
|
396
637
|
return undefined;
|
|
397
638
|
}
|
|
398
639
|
|
|
399
|
-
const request = this.
|
|
400
|
-
this.log.debug(`Simulating invalidate
|
|
640
|
+
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
641
|
+
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
642
|
+
|
|
643
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
401
644
|
|
|
402
645
|
try {
|
|
403
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
404
|
-
|
|
646
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
647
|
+
request,
|
|
648
|
+
undefined,
|
|
649
|
+
undefined,
|
|
650
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
651
|
+
);
|
|
652
|
+
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
653
|
+
...logData,
|
|
654
|
+
request,
|
|
655
|
+
gasUsed,
|
|
656
|
+
});
|
|
405
657
|
|
|
406
|
-
return {
|
|
658
|
+
return {
|
|
659
|
+
request,
|
|
660
|
+
gasUsed,
|
|
661
|
+
checkpointNumber,
|
|
662
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
663
|
+
reason,
|
|
664
|
+
};
|
|
407
665
|
} catch (err) {
|
|
408
666
|
const viemError = formatViemError(err);
|
|
409
667
|
|
|
410
|
-
// If the error is due to the
|
|
411
|
-
// we can safely ignore it and return undefined so we go ahead with
|
|
412
|
-
if (viemError.message?.includes('
|
|
668
|
+
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
669
|
+
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
670
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
413
671
|
this.log.verbose(
|
|
414
|
-
`Simulation for invalidate
|
|
672
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
415
673
|
{ ...logData, request, error: viemError.message },
|
|
416
674
|
);
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
this.log.verbose(`
|
|
675
|
+
const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
676
|
+
if (latestPendingCheckpointNumber < checkpointNumber) {
|
|
677
|
+
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
420
678
|
return undefined;
|
|
421
679
|
} else {
|
|
422
680
|
this.log.error(
|
|
423
|
-
`Simulation for invalidate ${
|
|
681
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
424
682
|
viemError,
|
|
425
683
|
logData,
|
|
426
684
|
);
|
|
427
|
-
throw new Error(
|
|
428
|
-
|
|
429
|
-
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
687
|
+
{
|
|
688
|
+
cause: viemError,
|
|
689
|
+
},
|
|
690
|
+
);
|
|
430
691
|
}
|
|
431
692
|
}
|
|
432
693
|
|
|
433
|
-
// Otherwise, throw. We cannot build the next
|
|
434
|
-
this.log.error(`Simulation for invalidate
|
|
435
|
-
|
|
694
|
+
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
695
|
+
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
696
|
+
this.backupFailedTx({
|
|
697
|
+
id: keccak256(request.data!),
|
|
698
|
+
failureType: 'simulation',
|
|
699
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
700
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
701
|
+
error: { message: viemError.message, name: viemError.name },
|
|
702
|
+
context: {
|
|
703
|
+
actions: [`invalidate-${reason}`],
|
|
704
|
+
checkpointNumber,
|
|
705
|
+
sender: this.getSenderAddress().toString(),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
436
709
|
}
|
|
437
710
|
}
|
|
438
711
|
|
|
439
|
-
private
|
|
712
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
440
713
|
if (validationResult.valid) {
|
|
441
|
-
throw new Error('Cannot invalidate a valid
|
|
714
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
442
715
|
}
|
|
443
716
|
|
|
444
|
-
const {
|
|
445
|
-
const logData = { ...
|
|
446
|
-
this.log.debug(`
|
|
717
|
+
const { checkpoint, committee, reason } = validationResult;
|
|
718
|
+
const logData = { ...checkpoint, reason };
|
|
719
|
+
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
447
720
|
|
|
448
721
|
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
449
722
|
validationResult.attestations,
|
|
@@ -451,14 +724,14 @@ export class SequencerPublisher {
|
|
|
451
724
|
|
|
452
725
|
if (reason === 'invalid-attestation') {
|
|
453
726
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
454
|
-
|
|
727
|
+
checkpoint.checkpointNumber,
|
|
455
728
|
attestationsAndSigners,
|
|
456
729
|
committee,
|
|
457
730
|
validationResult.invalidIndex,
|
|
458
731
|
);
|
|
459
732
|
} else if (reason === 'insufficient-attestations') {
|
|
460
733
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
461
|
-
|
|
734
|
+
checkpoint.checkpointNumber,
|
|
462
735
|
attestationsAndSigners,
|
|
463
736
|
committee,
|
|
464
737
|
);
|
|
@@ -468,48 +741,25 @@ export class SequencerPublisher {
|
|
|
468
741
|
}
|
|
469
742
|
}
|
|
470
743
|
|
|
471
|
-
/**
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
*
|
|
476
|
-
* @param block - The block to propose
|
|
477
|
-
* @param attestationData - The block's attestation data
|
|
478
|
-
*
|
|
479
|
-
*/
|
|
480
|
-
public async validateBlockForSubmission(
|
|
481
|
-
block: L2Block,
|
|
744
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
745
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
746
|
+
public async validateCheckpointForSubmission(
|
|
747
|
+
checkpoint: Checkpoint,
|
|
482
748
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
483
749
|
attestationsAndSignersSignature: Signature,
|
|
484
|
-
options: {
|
|
750
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
485
751
|
): Promise<bigint> {
|
|
486
752
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// so that the committee is recalculated correctly
|
|
490
|
-
const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
491
|
-
if (ignoreSignatures) {
|
|
492
|
-
const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber.toBigInt());
|
|
493
|
-
if (!committee) {
|
|
494
|
-
this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber.toBigInt()}`);
|
|
495
|
-
throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber.toBigInt()}`);
|
|
496
|
-
}
|
|
497
|
-
attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
498
|
-
CommitteeAttestation.fromAddress(committeeMember),
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const blobFields = block.getCheckpointBlobFields();
|
|
503
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
753
|
+
const blobFields = checkpoint.toBlobFields();
|
|
754
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
504
755
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
505
756
|
|
|
506
757
|
const args = [
|
|
507
758
|
{
|
|
508
|
-
header:
|
|
509
|
-
archive: toHex(
|
|
510
|
-
stateReference: block.header.state.toViem(),
|
|
759
|
+
header: checkpoint.header.toViem(),
|
|
760
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
511
761
|
oracleInput: {
|
|
512
|
-
feeAssetPriceModifier:
|
|
762
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
513
763
|
},
|
|
514
764
|
},
|
|
515
765
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -523,7 +773,7 @@ export class SequencerPublisher {
|
|
|
523
773
|
}
|
|
524
774
|
|
|
525
775
|
private async enqueueCastSignalHelper(
|
|
526
|
-
slotNumber:
|
|
776
|
+
slotNumber: SlotNumber,
|
|
527
777
|
timestamp: bigint,
|
|
528
778
|
signalType: GovernanceSignalAction,
|
|
529
779
|
payload: EthAddress,
|
|
@@ -545,10 +795,45 @@ export class SequencerPublisher {
|
|
|
545
795
|
const round = await base.computeRound(slotNumber);
|
|
546
796
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
547
797
|
|
|
798
|
+
if (roundInfo.quorumReached) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
|
|
548
802
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
549
803
|
return false;
|
|
550
804
|
}
|
|
551
805
|
|
|
806
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
807
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Check if payload was already submitted to governance
|
|
812
|
+
const cacheKey = payload.toString();
|
|
813
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
814
|
+
try {
|
|
815
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
816
|
+
const proposed = await retry(
|
|
817
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
818
|
+
'Check if payload was proposed',
|
|
819
|
+
makeBackoff([0, 1, 2]),
|
|
820
|
+
this.log,
|
|
821
|
+
true,
|
|
822
|
+
);
|
|
823
|
+
if (proposed) {
|
|
824
|
+
this.payloadProposedCache.add(cacheKey);
|
|
825
|
+
}
|
|
826
|
+
} catch (err) {
|
|
827
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
833
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
|
|
552
837
|
const cachedLastVote = this.lastActions[signalType];
|
|
553
838
|
this.lastActions[signalType] = slotNumber;
|
|
554
839
|
const action = signalType;
|
|
@@ -567,11 +852,26 @@ export class SequencerPublisher {
|
|
|
567
852
|
lastValidL2Slot: slotNumber,
|
|
568
853
|
});
|
|
569
854
|
|
|
855
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
856
|
+
|
|
570
857
|
try {
|
|
571
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
858
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
572
859
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
573
860
|
} catch (err) {
|
|
574
|
-
|
|
861
|
+
const viemError = formatViemError(err);
|
|
862
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
863
|
+
this.backupFailedTx({
|
|
864
|
+
id: keccak256(request.data!),
|
|
865
|
+
failureType: 'simulation',
|
|
866
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
867
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
868
|
+
error: { message: viemError.message, name: viemError.name },
|
|
869
|
+
context: {
|
|
870
|
+
actions: [action],
|
|
871
|
+
slot: slotNumber,
|
|
872
|
+
sender: this.getSenderAddress().toString(),
|
|
873
|
+
},
|
|
874
|
+
});
|
|
575
875
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
576
876
|
}
|
|
577
877
|
|
|
@@ -591,14 +891,14 @@ export class SequencerPublisher {
|
|
|
591
891
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
592
892
|
if (!success) {
|
|
593
893
|
this.log.error(
|
|
594
|
-
`Signaling in
|
|
894
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
595
895
|
logData,
|
|
596
896
|
);
|
|
597
897
|
this.lastActions[signalType] = cachedLastVote;
|
|
598
898
|
return false;
|
|
599
899
|
} else {
|
|
600
900
|
this.log.info(
|
|
601
|
-
`Signaling in
|
|
901
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
602
902
|
logData,
|
|
603
903
|
);
|
|
604
904
|
return true;
|
|
@@ -608,6 +908,17 @@ export class SequencerPublisher {
|
|
|
608
908
|
return true;
|
|
609
909
|
}
|
|
610
910
|
|
|
911
|
+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
912
|
+
const key = payload.toString();
|
|
913
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
914
|
+
if (cached) {
|
|
915
|
+
return cached;
|
|
916
|
+
}
|
|
917
|
+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
918
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
919
|
+
return isEmpty;
|
|
920
|
+
}
|
|
921
|
+
|
|
611
922
|
/**
|
|
612
923
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
613
924
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -616,7 +927,7 @@ export class SequencerPublisher {
|
|
|
616
927
|
*/
|
|
617
928
|
public enqueueGovernanceCastSignal(
|
|
618
929
|
governancePayload: EthAddress,
|
|
619
|
-
slotNumber:
|
|
930
|
+
slotNumber: SlotNumber,
|
|
620
931
|
timestamp: bigint,
|
|
621
932
|
signerAddress: EthAddress,
|
|
622
933
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
@@ -635,7 +946,7 @@ export class SequencerPublisher {
|
|
|
635
946
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
636
947
|
public async enqueueSlashingActions(
|
|
637
948
|
actions: ProposerSlashAction[],
|
|
638
|
-
slotNumber:
|
|
949
|
+
slotNumber: SlotNumber,
|
|
639
950
|
timestamp: bigint,
|
|
640
951
|
signerAddress: EthAddress,
|
|
641
952
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
@@ -755,31 +1066,25 @@ export class SequencerPublisher {
|
|
|
755
1066
|
return true;
|
|
756
1067
|
}
|
|
757
1068
|
|
|
758
|
-
/**
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
* @param block - L2 block to propose.
|
|
762
|
-
* @returns True if the tx has been enqueued, throws otherwise. See #9315
|
|
763
|
-
*/
|
|
764
|
-
public async enqueueProposeL2Block(
|
|
765
|
-
block: L2Block,
|
|
1069
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
1070
|
+
public async enqueueProposeCheckpoint(
|
|
1071
|
+
checkpoint: Checkpoint,
|
|
766
1072
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
767
1073
|
attestationsAndSignersSignature: Signature,
|
|
768
|
-
opts: { txTimeoutAt?: Date;
|
|
769
|
-
): Promise<
|
|
770
|
-
const checkpointHeader =
|
|
1074
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1075
|
+
): Promise<void> {
|
|
1076
|
+
const checkpointHeader = checkpoint.header;
|
|
771
1077
|
|
|
772
|
-
const blobFields =
|
|
773
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1078
|
+
const blobFields = checkpoint.toBlobFields();
|
|
1079
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
774
1080
|
|
|
775
|
-
const proposeTxArgs = {
|
|
1081
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
776
1082
|
header: checkpointHeader,
|
|
777
|
-
archive:
|
|
778
|
-
stateReference: block.header.state,
|
|
779
|
-
body: block.body.toBuffer(),
|
|
1083
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
780
1084
|
blobs,
|
|
781
1085
|
attestationsAndSigners,
|
|
782
1086
|
attestationsAndSignersSignature,
|
|
1087
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
783
1088
|
};
|
|
784
1089
|
|
|
785
1090
|
let ts: bigint;
|
|
@@ -790,22 +1095,29 @@ export class SequencerPublisher {
|
|
|
790
1095
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
791
1096
|
// make time consistency checks break.
|
|
792
1097
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
793
|
-
ts = await this.
|
|
1098
|
+
ts = await this.validateCheckpointForSubmission(
|
|
1099
|
+
checkpoint,
|
|
1100
|
+
attestationsAndSigners,
|
|
1101
|
+
attestationsAndSignersSignature,
|
|
1102
|
+
opts,
|
|
1103
|
+
);
|
|
794
1104
|
} catch (err: any) {
|
|
795
|
-
this.log.error(`
|
|
796
|
-
...
|
|
797
|
-
slotNumber:
|
|
798
|
-
|
|
1105
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1106
|
+
...checkpoint.getStats(),
|
|
1107
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
1108
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
799
1109
|
});
|
|
800
1110
|
throw err;
|
|
801
1111
|
}
|
|
802
1112
|
|
|
803
|
-
this.log.verbose(`Enqueuing
|
|
804
|
-
await this.addProposeTx(
|
|
805
|
-
return true;
|
|
1113
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1114
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
806
1115
|
}
|
|
807
1116
|
|
|
808
|
-
public
|
|
1117
|
+
public enqueueInvalidateCheckpoint(
|
|
1118
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
1119
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
1120
|
+
) {
|
|
809
1121
|
if (!request) {
|
|
810
1122
|
return;
|
|
811
1123
|
}
|
|
@@ -813,24 +1125,24 @@ export class SequencerPublisher {
|
|
|
813
1125
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
814
1126
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
815
1127
|
|
|
816
|
-
const { gasUsed,
|
|
817
|
-
const logData = { gasUsed,
|
|
818
|
-
this.log.verbose(`Enqueuing invalidate
|
|
1128
|
+
const { gasUsed, checkpointNumber } = request;
|
|
1129
|
+
const logData = { gasUsed, checkpointNumber, gasLimit, opts };
|
|
1130
|
+
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
819
1131
|
this.addRequest({
|
|
820
1132
|
action: `invalidate-by-${request.reason}`,
|
|
821
1133
|
request: request.request,
|
|
822
1134
|
gasConfig: { gasLimit, txTimeoutAt: opts.txTimeoutAt },
|
|
823
|
-
lastValidL2Slot: this.getCurrentL2Slot() +
|
|
1135
|
+
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
824
1136
|
checkSuccess: (_req, result) => {
|
|
825
1137
|
const success =
|
|
826
1138
|
result &&
|
|
827
1139
|
result.receipt &&
|
|
828
1140
|
result.receipt.status === 'success' &&
|
|
829
|
-
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
1141
|
+
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
830
1142
|
if (!success) {
|
|
831
|
-
this.log.warn(`Invalidate
|
|
1143
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
832
1144
|
} else {
|
|
833
|
-
this.log.info(`Invalidate
|
|
1145
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
834
1146
|
}
|
|
835
1147
|
return !!success;
|
|
836
1148
|
},
|
|
@@ -841,7 +1153,7 @@ export class SequencerPublisher {
|
|
|
841
1153
|
action: Action,
|
|
842
1154
|
request: L1TxRequest,
|
|
843
1155
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
844
|
-
slotNumber:
|
|
1156
|
+
slotNumber: SlotNumber,
|
|
845
1157
|
timestamp: bigint,
|
|
846
1158
|
) {
|
|
847
1159
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
@@ -855,13 +1167,30 @@ export class SequencerPublisher {
|
|
|
855
1167
|
|
|
856
1168
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
857
1169
|
|
|
1170
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1171
|
+
|
|
858
1172
|
let gasUsed: bigint;
|
|
1173
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
859
1174
|
try {
|
|
860
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1175
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
861
1176
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
862
1177
|
} catch (err) {
|
|
863
|
-
const viemError = formatViemError(err);
|
|
1178
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
864
1179
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1180
|
+
|
|
1181
|
+
this.backupFailedTx({
|
|
1182
|
+
id: keccak256(request.data!),
|
|
1183
|
+
failureType: 'simulation',
|
|
1184
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1185
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1186
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1187
|
+
context: {
|
|
1188
|
+
actions: [action],
|
|
1189
|
+
slot: slotNumber,
|
|
1190
|
+
sender: this.getSenderAddress().toString(),
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
|
|
865
1194
|
return false;
|
|
866
1195
|
}
|
|
867
1196
|
|
|
@@ -869,10 +1198,14 @@ export class SequencerPublisher {
|
|
|
869
1198
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
870
1199
|
logData.gasLimit = gasLimit;
|
|
871
1200
|
|
|
1201
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1202
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1203
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1204
|
+
|
|
872
1205
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
873
1206
|
this.addRequest({
|
|
874
1207
|
action,
|
|
875
|
-
request,
|
|
1208
|
+
request: requestWithAbi,
|
|
876
1209
|
gasConfig: { gasLimit },
|
|
877
1210
|
lastValidL2Slot: slotNumber,
|
|
878
1211
|
checkSuccess: (_req, result) => {
|
|
@@ -909,44 +1242,70 @@ export class SequencerPublisher {
|
|
|
909
1242
|
private async prepareProposeTx(
|
|
910
1243
|
encodedData: L1ProcessArgs,
|
|
911
1244
|
timestamp: bigint,
|
|
912
|
-
options: {
|
|
1245
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
913
1246
|
) {
|
|
914
1247
|
const kzg = Blob.getViemKzgInstance();
|
|
915
1248
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
916
1249
|
this.log.debug('Validating blob input', { blobInput });
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1250
|
+
|
|
1251
|
+
// Get blob evaluation gas
|
|
1252
|
+
let blobEvaluationGas: bigint;
|
|
1253
|
+
if (this.config.fishermanMode) {
|
|
1254
|
+
// In fisherman mode, we can't estimate blob gas because estimateGas doesn't support state overrides
|
|
1255
|
+
// Use a fixed estimate.
|
|
1256
|
+
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
1257
|
+
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
1258
|
+
} else {
|
|
1259
|
+
// Normal mode - use estimateGas with blob inputs
|
|
1260
|
+
blobEvaluationGas = await this.l1TxUtils
|
|
1261
|
+
.estimateGas(
|
|
1262
|
+
this.getSenderAddress().toString(),
|
|
1263
|
+
{
|
|
1264
|
+
to: this.rollupContract.address,
|
|
1265
|
+
data: encodeFunctionData({
|
|
1266
|
+
abi: RollupAbi,
|
|
1267
|
+
functionName: 'validateBlobs',
|
|
1268
|
+
args: [blobInput],
|
|
1269
|
+
}),
|
|
1270
|
+
},
|
|
1271
|
+
{},
|
|
1272
|
+
{
|
|
1273
|
+
blobs: encodedData.blobs.map(b => b.data),
|
|
1274
|
+
kzg,
|
|
1275
|
+
},
|
|
1276
|
+
)
|
|
1277
|
+
.catch(async err => {
|
|
1278
|
+
const viemError = formatViemError(err);
|
|
1279
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1280
|
+
const validateBlobsData = encodeFunctionData({
|
|
923
1281
|
abi: RollupAbi,
|
|
924
1282
|
functionName: 'validateBlobs',
|
|
925
1283
|
args: [blobInput],
|
|
926
|
-
})
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1284
|
+
});
|
|
1285
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1286
|
+
this.backupFailedTx({
|
|
1287
|
+
id: keccak256(validateBlobsData),
|
|
1288
|
+
failureType: 'simulation',
|
|
1289
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1290
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1291
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1292
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1293
|
+
context: {
|
|
1294
|
+
actions: ['validate-blobs'],
|
|
1295
|
+
sender: this.getSenderAddress().toString(),
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1298
|
+
throw new Error('Failed to validate blobs');
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
940
1301
|
const signers = encodedData.attestationsAndSigners.getSigners().map(signer => signer.toString());
|
|
941
1302
|
|
|
942
1303
|
const args = [
|
|
943
1304
|
{
|
|
944
1305
|
header: encodedData.header.toViem(),
|
|
945
1306
|
archive: toHex(encodedData.archive),
|
|
946
|
-
stateReference: encodedData.stateReference.toViem(),
|
|
947
1307
|
oracleInput: {
|
|
948
|
-
|
|
949
|
-
feeAssetPriceModifier: 0n,
|
|
1308
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
950
1309
|
},
|
|
951
1310
|
},
|
|
952
1311
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -971,9 +1330,8 @@ export class SequencerPublisher {
|
|
|
971
1330
|
{
|
|
972
1331
|
readonly header: ViemHeader;
|
|
973
1332
|
readonly archive: `0x${string}`;
|
|
974
|
-
readonly stateReference: ViemStateReference;
|
|
975
1333
|
readonly oracleInput: {
|
|
976
|
-
readonly feeAssetPriceModifier:
|
|
1334
|
+
readonly feeAssetPriceModifier: bigint;
|
|
977
1335
|
};
|
|
978
1336
|
},
|
|
979
1337
|
ViemCommitteeAttestations,
|
|
@@ -982,7 +1340,7 @@ export class SequencerPublisher {
|
|
|
982
1340
|
`0x${string}`,
|
|
983
1341
|
],
|
|
984
1342
|
timestamp: bigint,
|
|
985
|
-
options: {
|
|
1343
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
986
1344
|
) {
|
|
987
1345
|
const rollupData = encodeFunctionData({
|
|
988
1346
|
abi: RollupAbi,
|
|
@@ -990,44 +1348,78 @@ export class SequencerPublisher {
|
|
|
990
1348
|
args,
|
|
991
1349
|
});
|
|
992
1350
|
|
|
993
|
-
// override the pending
|
|
994
|
-
const
|
|
995
|
-
options.
|
|
996
|
-
? await this.rollupContract.
|
|
1351
|
+
// override the pending checkpoint number if requested
|
|
1352
|
+
const forcePendingCheckpointNumberStateDiff = (
|
|
1353
|
+
options.forcePendingCheckpointNumber !== undefined
|
|
1354
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
997
1355
|
: []
|
|
998
1356
|
).flatMap(override => override.stateDiff ?? []);
|
|
999
1357
|
|
|
1358
|
+
const stateOverrides: StateOverride = [
|
|
1359
|
+
{
|
|
1360
|
+
address: this.rollupContract.address,
|
|
1361
|
+
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1362
|
+
stateDiff: [
|
|
1363
|
+
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1364
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1365
|
+
],
|
|
1366
|
+
},
|
|
1367
|
+
];
|
|
1368
|
+
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1369
|
+
if (this.proposerAddressForSimulation) {
|
|
1370
|
+
stateOverrides.push({
|
|
1371
|
+
address: this.proposerAddressForSimulation.toString(),
|
|
1372
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1377
|
+
|
|
1000
1378
|
const simulationResult = await this.l1TxUtils
|
|
1001
1379
|
.simulate(
|
|
1002
1380
|
{
|
|
1003
1381
|
to: this.rollupContract.address,
|
|
1004
1382
|
data: rollupData,
|
|
1005
|
-
gas:
|
|
1383
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1384
|
+
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1006
1385
|
},
|
|
1007
1386
|
{
|
|
1008
1387
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1009
1388
|
time: timestamp + 1n,
|
|
1010
1389
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1011
|
-
gasLimit:
|
|
1390
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1012
1391
|
},
|
|
1013
|
-
|
|
1014
|
-
{
|
|
1015
|
-
address: this.rollupContract.address,
|
|
1016
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1017
|
-
stateDiff: [
|
|
1018
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1019
|
-
...forcePendingBlockNumberStateDiff,
|
|
1020
|
-
],
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1392
|
+
stateOverrides,
|
|
1023
1393
|
RollupAbi,
|
|
1024
1394
|
{
|
|
1025
1395
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1026
|
-
fallbackGasEstimate:
|
|
1396
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1027
1397
|
},
|
|
1028
1398
|
)
|
|
1029
1399
|
.catch(err => {
|
|
1030
|
-
|
|
1400
|
+
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1401
|
+
const viemError = formatViemError(err);
|
|
1402
|
+
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1403
|
+
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1404
|
+
// Return a minimal simulation result with the fallback gas estimate
|
|
1405
|
+
return {
|
|
1406
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1407
|
+
logs: [],
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1411
|
+
this.backupFailedTx({
|
|
1412
|
+
id: keccak256(rollupData),
|
|
1413
|
+
failureType: 'simulation',
|
|
1414
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1415
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1416
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1417
|
+
context: {
|
|
1418
|
+
actions: ['propose'],
|
|
1419
|
+
slot: Number(args[0].header.slotNumber),
|
|
1420
|
+
sender: this.getSenderAddress().toString(),
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1031
1423
|
throw err;
|
|
1032
1424
|
});
|
|
1033
1425
|
|
|
@@ -1035,11 +1427,12 @@ export class SequencerPublisher {
|
|
|
1035
1427
|
}
|
|
1036
1428
|
|
|
1037
1429
|
private async addProposeTx(
|
|
1038
|
-
|
|
1430
|
+
checkpoint: Checkpoint,
|
|
1039
1431
|
encodedData: L1ProcessArgs,
|
|
1040
|
-
opts: { txTimeoutAt?: Date;
|
|
1432
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1041
1433
|
timestamp: bigint,
|
|
1042
1434
|
): Promise<void> {
|
|
1435
|
+
const slot = checkpoint.header.slotNumber;
|
|
1043
1436
|
const timer = new Timer();
|
|
1044
1437
|
const kzg = Blob.getViemKzgInstance();
|
|
1045
1438
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
@@ -1054,11 +1447,13 @@ export class SequencerPublisher {
|
|
|
1054
1447
|
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1055
1448
|
);
|
|
1056
1449
|
|
|
1057
|
-
// Send the blobs to the blob
|
|
1058
|
-
// tx fails but it does get mined. We make sure that the blobs are sent to the blob
|
|
1059
|
-
void
|
|
1060
|
-
this.
|
|
1061
|
-
|
|
1450
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1451
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
1452
|
+
void Promise.resolve().then(() =>
|
|
1453
|
+
this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
|
|
1454
|
+
this.log.error('Failed to send blobs to blob client');
|
|
1455
|
+
}),
|
|
1456
|
+
);
|
|
1062
1457
|
|
|
1063
1458
|
return this.addRequest({
|
|
1064
1459
|
action: 'propose',
|
|
@@ -1066,7 +1461,7 @@ export class SequencerPublisher {
|
|
|
1066
1461
|
to: this.rollupContract.address,
|
|
1067
1462
|
data: rollupData,
|
|
1068
1463
|
},
|
|
1069
|
-
lastValidL2Slot:
|
|
1464
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1070
1465
|
gasConfig: { ...opts, gasLimit },
|
|
1071
1466
|
blobConfig: {
|
|
1072
1467
|
blobs: encodedData.blobs.map(b => b.data),
|
|
@@ -1080,12 +1475,13 @@ export class SequencerPublisher {
|
|
|
1080
1475
|
const success =
|
|
1081
1476
|
receipt &&
|
|
1082
1477
|
receipt.status === 'success' &&
|
|
1083
|
-
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
1478
|
+
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1479
|
+
|
|
1084
1480
|
if (success) {
|
|
1085
1481
|
const endBlock = receipt.blockNumber;
|
|
1086
1482
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1087
1483
|
const { calldataGas, calldataSize, sender } = stats!;
|
|
1088
|
-
const publishStats:
|
|
1484
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1089
1485
|
gasPrice: receipt.effectiveGasPrice,
|
|
1090
1486
|
gasUsed: receipt.gasUsed,
|
|
1091
1487
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
@@ -1094,23 +1490,26 @@ export class SequencerPublisher {
|
|
|
1094
1490
|
calldataGas,
|
|
1095
1491
|
calldataSize,
|
|
1096
1492
|
sender,
|
|
1097
|
-
...
|
|
1493
|
+
...checkpoint.getStats(),
|
|
1098
1494
|
eventName: 'rollup-published-to-l1',
|
|
1099
1495
|
blobCount: encodedData.blobs.length,
|
|
1100
1496
|
inclusionBlocks,
|
|
1101
1497
|
};
|
|
1102
|
-
this.log.info(`Published
|
|
1498
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
1499
|
+
...stats,
|
|
1500
|
+
...checkpoint.getStats(),
|
|
1501
|
+
...pick(receipt, 'transactionHash', 'blockHash'),
|
|
1502
|
+
});
|
|
1103
1503
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1104
1504
|
|
|
1105
1505
|
return true;
|
|
1106
1506
|
} else {
|
|
1107
1507
|
this.metrics.recordFailedTx('process');
|
|
1108
|
-
this.log.error(
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
});
|
|
1508
|
+
this.log.error(
|
|
1509
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1510
|
+
undefined,
|
|
1511
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1512
|
+
);
|
|
1114
1513
|
return false;
|
|
1115
1514
|
}
|
|
1116
1515
|
},
|