@aztec/sequencer-client 0.0.1-commit.9593d84 → 0.0.1-commit.967fc6998
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 +21 -16
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +45 -27
- package/dest/config.d.ts +14 -8
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +83 -35
- package/dest/global_variable_builder/global_builder.d.ts +20 -13
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +51 -41
- 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 +41 -20
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +109 -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 +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 +69 -47
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +847 -145
- 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/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 +38 -6
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +216 -72
- package/dest/sequencer/sequencer.d.ts +119 -133
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +717 -625
- 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 +32 -30
- package/src/client/sequencer-client.ts +54 -47
- package/src/config.ts +95 -44
- package/src/global_variable_builder/global_builder.ts +65 -61
- package/src/index.ts +1 -7
- package/src/publisher/config.ts +131 -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 +28 -10
- package/src/publisher/sequencer-publisher-metrics.ts +19 -71
- package/src/publisher/sequencer-publisher.ts +528 -184
- 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 +268 -82
- package/src/sequencer/sequencer.ts +464 -831
- 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 -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,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,
|
|
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,
|
|
21
25
|
WEI_CONST,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from '@aztec/ethereum';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
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';
|
|
28
|
-
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
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';
|
|
29
33
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
30
34
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
31
|
-
import type { Fr } from '@aztec/foundation/fields';
|
|
32
35
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
36
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
33
37
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
34
38
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
35
39
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
36
40
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
37
|
-
import {
|
|
41
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
42
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
38
43
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
39
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
40
|
-
import type {
|
|
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 */
|
|
@@ -57,6 +72,8 @@ type L1ProcessArgs = {
|
|
|
57
72
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
58
73
|
/** Attestations and signers signature */
|
|
59
74
|
attestationsAndSignersSignature: Signature;
|
|
75
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
76
|
+
feeAssetPriceModifier: bigint;
|
|
60
77
|
};
|
|
61
78
|
|
|
62
79
|
export const Actions = [
|
|
@@ -78,12 +95,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
|
|
|
78
95
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
79
96
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
80
97
|
|
|
81
|
-
export type
|
|
98
|
+
export type InvalidateCheckpointRequest = {
|
|
82
99
|
request: L1TxRequest;
|
|
83
100
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
84
101
|
gasUsed: bigint;
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
checkpointNumber: CheckpointNumber;
|
|
103
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
87
104
|
};
|
|
88
105
|
|
|
89
106
|
interface RequestWithExpiry {
|
|
@@ -102,23 +119,29 @@ export class SequencerPublisher {
|
|
|
102
119
|
private interrupted = false;
|
|
103
120
|
private metrics: SequencerPublisherMetrics;
|
|
104
121
|
public epochCache: EpochCache;
|
|
122
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
105
123
|
|
|
106
124
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
107
125
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
108
126
|
|
|
109
127
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
110
128
|
|
|
129
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
130
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
131
|
+
|
|
111
132
|
protected log: Logger;
|
|
112
133
|
protected ethereumSlotDuration: bigint;
|
|
113
134
|
|
|
114
|
-
private
|
|
135
|
+
private blobClient: BlobClientInterface;
|
|
115
136
|
|
|
116
137
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
117
138
|
private proposerAddressForSimulation?: EthAddress;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
122
145
|
|
|
123
146
|
// A CALL to a cold address is 2700 gas
|
|
124
147
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -126,20 +149,23 @@ export class SequencerPublisher {
|
|
|
126
149
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
127
150
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
128
151
|
|
|
129
|
-
public l1TxUtils:
|
|
152
|
+
public l1TxUtils: L1TxUtils;
|
|
130
153
|
public rollupContract: RollupContract;
|
|
131
154
|
public govProposerContract: GovernanceProposerContract;
|
|
132
155
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
133
156
|
public slashFactoryContract: SlashFactoryContract;
|
|
134
157
|
|
|
158
|
+
public readonly tracer: Tracer;
|
|
159
|
+
|
|
135
160
|
protected requests: RequestWithExpiry[] = [];
|
|
136
161
|
|
|
137
162
|
constructor(
|
|
138
|
-
private config:
|
|
163
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
164
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
139
165
|
deps: {
|
|
140
166
|
telemetry?: TelemetryClient;
|
|
141
|
-
|
|
142
|
-
l1TxUtils:
|
|
167
|
+
blobClient: BlobClientInterface;
|
|
168
|
+
l1TxUtils: L1TxUtils;
|
|
143
169
|
rollupContract: RollupContract;
|
|
144
170
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
145
171
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -156,11 +182,11 @@ export class SequencerPublisher {
|
|
|
156
182
|
this.epochCache = deps.epochCache;
|
|
157
183
|
this.lastActions = deps.lastActions;
|
|
158
184
|
|
|
159
|
-
this.
|
|
160
|
-
deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
|
|
185
|
+
this.blobClient = deps.blobClient;
|
|
161
186
|
|
|
162
187
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
163
188
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
189
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
164
190
|
this.l1TxUtils = deps.l1TxUtils;
|
|
165
191
|
|
|
166
192
|
this.rollupContract = deps.rollupContract;
|
|
@@ -174,16 +200,72 @@ export class SequencerPublisher {
|
|
|
174
200
|
this.slashingProposerContract = newSlashingProposer;
|
|
175
201
|
});
|
|
176
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
|
+
});
|
|
177
244
|
}
|
|
178
245
|
|
|
179
246
|
public getRollupContract(): RollupContract {
|
|
180
247
|
return this.rollupContract;
|
|
181
248
|
}
|
|
182
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
|
+
|
|
183
258
|
public getSenderAddress() {
|
|
184
259
|
return this.l1TxUtils.getSenderAddress();
|
|
185
260
|
}
|
|
186
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
|
+
|
|
187
269
|
/**
|
|
188
270
|
* Sets the proposer address to use for simulations in fisherman mode.
|
|
189
271
|
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
@@ -211,6 +293,62 @@ export class SequencerPublisher {
|
|
|
211
293
|
}
|
|
212
294
|
}
|
|
213
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
|
+
|
|
214
352
|
/**
|
|
215
353
|
* Sends all requests that are still valid.
|
|
216
354
|
* @returns one of:
|
|
@@ -218,10 +356,11 @@ export class SequencerPublisher {
|
|
|
218
356
|
* - a receipt and errorMsg if it failed on L1
|
|
219
357
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
220
358
|
*/
|
|
359
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
221
360
|
public async sendRequests() {
|
|
222
361
|
const requestsToProcess = [...this.requests];
|
|
223
362
|
this.requests = [];
|
|
224
|
-
if (this.interrupted) {
|
|
363
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
225
364
|
return undefined;
|
|
226
365
|
}
|
|
227
366
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -264,7 +403,16 @@ export class SequencerPublisher {
|
|
|
264
403
|
|
|
265
404
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
266
405
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
267
|
-
|
|
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
|
+
}
|
|
268
416
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
269
417
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
270
418
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -274,6 +422,21 @@ export class SequencerPublisher {
|
|
|
274
422
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
275
423
|
|
|
276
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
|
+
|
|
277
440
|
this.log.debug('Forwarding transactions', {
|
|
278
441
|
validRequests: validRequests.map(request => request.action),
|
|
279
442
|
txConfig,
|
|
@@ -286,7 +449,12 @@ export class SequencerPublisher {
|
|
|
286
449
|
this.rollupContract.address,
|
|
287
450
|
this.log,
|
|
288
451
|
);
|
|
289
|
-
const
|
|
452
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
453
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
454
|
+
validRequests,
|
|
455
|
+
result,
|
|
456
|
+
txContext,
|
|
457
|
+
);
|
|
290
458
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
291
459
|
} catch (err) {
|
|
292
460
|
const viemError = formatViemError(err);
|
|
@@ -306,11 +474,25 @@ export class SequencerPublisher {
|
|
|
306
474
|
|
|
307
475
|
private callbackBundledTransactions(
|
|
308
476
|
requests: RequestWithExpiry[],
|
|
309
|
-
result
|
|
477
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
478
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
310
479
|
) {
|
|
311
480
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
312
481
|
if (result instanceof FormattedViemError) {
|
|
313
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
|
+
});
|
|
314
496
|
return { failedActions: requests.map(r => r.action) };
|
|
315
497
|
} else {
|
|
316
498
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -323,6 +505,30 @@ export class SequencerPublisher {
|
|
|
323
505
|
failedActions.push(request.action);
|
|
324
506
|
}
|
|
325
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
|
+
}
|
|
326
532
|
return { successfulActions, failedActions };
|
|
327
533
|
}
|
|
328
534
|
}
|
|
@@ -335,14 +541,14 @@ export class SequencerPublisher {
|
|
|
335
541
|
public canProposeAtNextEthBlock(
|
|
336
542
|
tipArchive: Fr,
|
|
337
543
|
msgSender: EthAddress,
|
|
338
|
-
opts: {
|
|
544
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
339
545
|
) {
|
|
340
546
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
341
547
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
342
548
|
|
|
343
549
|
return this.rollupContract
|
|
344
550
|
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
345
|
-
forcePendingCheckpointNumber: opts.
|
|
551
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
346
552
|
})
|
|
347
553
|
.catch(err => {
|
|
348
554
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
@@ -361,7 +567,11 @@ export class SequencerPublisher {
|
|
|
361
567
|
* It will throw if the block header is invalid.
|
|
362
568
|
* @param header - The block header to validate
|
|
363
569
|
*/
|
|
364
|
-
|
|
570
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
571
|
+
public async validateBlockHeader(
|
|
572
|
+
header: CheckpointHeader,
|
|
573
|
+
opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
|
|
574
|
+
): Promise<void> {
|
|
365
575
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
366
576
|
|
|
367
577
|
const args = [
|
|
@@ -370,12 +580,14 @@ export class SequencerPublisher {
|
|
|
370
580
|
[], // no signers
|
|
371
581
|
Signature.empty().toViemSignature(),
|
|
372
582
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
373
|
-
header.
|
|
583
|
+
header.blobsHash.toString(),
|
|
374
584
|
flags,
|
|
375
585
|
] as const;
|
|
376
586
|
|
|
377
587
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
378
|
-
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
588
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
589
|
+
opts?.forcePendingCheckpointNumber,
|
|
590
|
+
);
|
|
379
591
|
let balance = 0n;
|
|
380
592
|
if (this.config.fishermanMode) {
|
|
381
593
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -402,77 +614,109 @@ export class SequencerPublisher {
|
|
|
402
614
|
}
|
|
403
615
|
|
|
404
616
|
/**
|
|
405
|
-
* Simulate making a call to invalidate a
|
|
406
|
-
* @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)
|
|
407
619
|
*/
|
|
408
|
-
public async
|
|
409
|
-
validationResult:
|
|
410
|
-
): Promise<
|
|
620
|
+
public async simulateInvalidateCheckpoint(
|
|
621
|
+
validationResult: ValidateCheckpointResult,
|
|
622
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
411
623
|
if (validationResult.valid) {
|
|
412
624
|
return undefined;
|
|
413
625
|
}
|
|
414
626
|
|
|
415
|
-
const { reason,
|
|
416
|
-
const
|
|
417
|
-
const logData = { ...
|
|
627
|
+
const { reason, checkpoint } = validationResult;
|
|
628
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
629
|
+
const logData = { ...checkpoint, reason };
|
|
418
630
|
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
631
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
632
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
421
633
|
this.log.verbose(
|
|
422
|
-
`Skipping
|
|
423
|
-
{
|
|
634
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
635
|
+
{ currentCheckpointNumber, ...logData },
|
|
424
636
|
);
|
|
425
637
|
return undefined;
|
|
426
638
|
}
|
|
427
639
|
|
|
428
|
-
const request = this.
|
|
429
|
-
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();
|
|
430
644
|
|
|
431
645
|
try {
|
|
432
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
433
|
-
|
|
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
|
+
});
|
|
434
657
|
|
|
435
|
-
return {
|
|
658
|
+
return {
|
|
659
|
+
request,
|
|
660
|
+
gasUsed,
|
|
661
|
+
checkpointNumber,
|
|
662
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
663
|
+
reason,
|
|
664
|
+
};
|
|
436
665
|
} catch (err) {
|
|
437
666
|
const viemError = formatViemError(err);
|
|
438
667
|
|
|
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('
|
|
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')) {
|
|
442
671
|
this.log.verbose(
|
|
443
|
-
`Simulation for invalidate
|
|
672
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
444
673
|
{ ...logData, request, error: viemError.message },
|
|
445
674
|
);
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
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 });
|
|
449
678
|
return undefined;
|
|
450
679
|
} else {
|
|
451
680
|
this.log.error(
|
|
452
|
-
`Simulation for invalidate ${
|
|
681
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
453
682
|
viemError,
|
|
454
683
|
logData,
|
|
455
684
|
);
|
|
456
|
-
throw new Error(
|
|
457
|
-
|
|
458
|
-
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
687
|
+
{
|
|
688
|
+
cause: viemError,
|
|
689
|
+
},
|
|
690
|
+
);
|
|
459
691
|
}
|
|
460
692
|
}
|
|
461
693
|
|
|
462
|
-
// Otherwise, throw. We cannot build the next
|
|
463
|
-
this.log.error(`Simulation for invalidate
|
|
464
|
-
|
|
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 });
|
|
465
709
|
}
|
|
466
710
|
}
|
|
467
711
|
|
|
468
|
-
private
|
|
712
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
469
713
|
if (validationResult.valid) {
|
|
470
|
-
throw new Error('Cannot invalidate a valid
|
|
714
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
471
715
|
}
|
|
472
716
|
|
|
473
|
-
const {
|
|
474
|
-
const logData = { ...
|
|
475
|
-
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);
|
|
476
720
|
|
|
477
721
|
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
478
722
|
validationResult.attestations,
|
|
@@ -480,14 +724,14 @@ export class SequencerPublisher {
|
|
|
480
724
|
|
|
481
725
|
if (reason === 'invalid-attestation') {
|
|
482
726
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
483
|
-
|
|
727
|
+
checkpoint.checkpointNumber,
|
|
484
728
|
attestationsAndSigners,
|
|
485
729
|
committee,
|
|
486
730
|
validationResult.invalidIndex,
|
|
487
731
|
);
|
|
488
732
|
} else if (reason === 'insufficient-attestations') {
|
|
489
733
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
490
|
-
|
|
734
|
+
checkpoint.checkpointNumber,
|
|
491
735
|
attestationsAndSigners,
|
|
492
736
|
committee,
|
|
493
737
|
);
|
|
@@ -497,47 +741,25 @@ export class SequencerPublisher {
|
|
|
497
741
|
}
|
|
498
742
|
}
|
|
499
743
|
|
|
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,
|
|
744
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
745
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
746
|
+
public async validateCheckpointForSubmission(
|
|
747
|
+
checkpoint: Checkpoint,
|
|
511
748
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
512
749
|
attestationsAndSignersSignature: Signature,
|
|
513
|
-
options: {
|
|
750
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
514
751
|
): Promise<bigint> {
|
|
515
752
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
516
|
-
|
|
517
|
-
|
|
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);
|
|
753
|
+
const blobFields = checkpoint.toBlobFields();
|
|
754
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
533
755
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
534
756
|
|
|
535
757
|
const args = [
|
|
536
758
|
{
|
|
537
|
-
header:
|
|
538
|
-
archive: toHex(
|
|
759
|
+
header: checkpoint.header.toViem(),
|
|
760
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
539
761
|
oracleInput: {
|
|
540
|
-
feeAssetPriceModifier:
|
|
762
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
541
763
|
},
|
|
542
764
|
},
|
|
543
765
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -573,10 +795,45 @@ export class SequencerPublisher {
|
|
|
573
795
|
const round = await base.computeRound(slotNumber);
|
|
574
796
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
575
797
|
|
|
798
|
+
if (roundInfo.quorumReached) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
|
|
576
802
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
577
803
|
return false;
|
|
578
804
|
}
|
|
579
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
|
+
|
|
580
837
|
const cachedLastVote = this.lastActions[signalType];
|
|
581
838
|
this.lastActions[signalType] = slotNumber;
|
|
582
839
|
const action = signalType;
|
|
@@ -595,11 +852,26 @@ export class SequencerPublisher {
|
|
|
595
852
|
lastValidL2Slot: slotNumber,
|
|
596
853
|
});
|
|
597
854
|
|
|
855
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
856
|
+
|
|
598
857
|
try {
|
|
599
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
858
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
600
859
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
601
860
|
} catch (err) {
|
|
602
|
-
|
|
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
|
+
});
|
|
603
875
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
604
876
|
}
|
|
605
877
|
|
|
@@ -619,14 +891,14 @@ export class SequencerPublisher {
|
|
|
619
891
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
620
892
|
if (!success) {
|
|
621
893
|
this.log.error(
|
|
622
|
-
`Signaling in
|
|
894
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
623
895
|
logData,
|
|
624
896
|
);
|
|
625
897
|
this.lastActions[signalType] = cachedLastVote;
|
|
626
898
|
return false;
|
|
627
899
|
} else {
|
|
628
900
|
this.log.info(
|
|
629
|
-
`Signaling in
|
|
901
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
630
902
|
logData,
|
|
631
903
|
);
|
|
632
904
|
return true;
|
|
@@ -636,6 +908,17 @@ export class SequencerPublisher {
|
|
|
636
908
|
return true;
|
|
637
909
|
}
|
|
638
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
|
+
|
|
639
922
|
/**
|
|
640
923
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
641
924
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -783,30 +1066,25 @@ export class SequencerPublisher {
|
|
|
783
1066
|
return true;
|
|
784
1067
|
}
|
|
785
1068
|
|
|
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,
|
|
1069
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
1070
|
+
public async enqueueProposeCheckpoint(
|
|
1071
|
+
checkpoint: Checkpoint,
|
|
794
1072
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
795
1073
|
attestationsAndSignersSignature: Signature,
|
|
796
|
-
opts: { txTimeoutAt?: Date;
|
|
797
|
-
): Promise<
|
|
798
|
-
const checkpointHeader =
|
|
1074
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1075
|
+
): Promise<void> {
|
|
1076
|
+
const checkpointHeader = checkpoint.header;
|
|
799
1077
|
|
|
800
|
-
const blobFields =
|
|
801
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1078
|
+
const blobFields = checkpoint.toBlobFields();
|
|
1079
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
802
1080
|
|
|
803
|
-
const proposeTxArgs = {
|
|
1081
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
804
1082
|
header: checkpointHeader,
|
|
805
|
-
archive:
|
|
806
|
-
body: block.body.toBuffer(),
|
|
1083
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
807
1084
|
blobs,
|
|
808
1085
|
attestationsAndSigners,
|
|
809
1086
|
attestationsAndSignersSignature,
|
|
1087
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
810
1088
|
};
|
|
811
1089
|
|
|
812
1090
|
let ts: bigint;
|
|
@@ -817,22 +1095,29 @@ export class SequencerPublisher {
|
|
|
817
1095
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
818
1096
|
// make time consistency checks break.
|
|
819
1097
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
820
|
-
ts = await this.
|
|
1098
|
+
ts = await this.validateCheckpointForSubmission(
|
|
1099
|
+
checkpoint,
|
|
1100
|
+
attestationsAndSigners,
|
|
1101
|
+
attestationsAndSignersSignature,
|
|
1102
|
+
opts,
|
|
1103
|
+
);
|
|
821
1104
|
} catch (err: any) {
|
|
822
|
-
this.log.error(`
|
|
823
|
-
...
|
|
824
|
-
slotNumber:
|
|
825
|
-
|
|
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,
|
|
826
1109
|
});
|
|
827
1110
|
throw err;
|
|
828
1111
|
}
|
|
829
1112
|
|
|
830
|
-
this.log.verbose(`Enqueuing
|
|
831
|
-
await this.addProposeTx(
|
|
832
|
-
return true;
|
|
1113
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1114
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
833
1115
|
}
|
|
834
1116
|
|
|
835
|
-
public
|
|
1117
|
+
public enqueueInvalidateCheckpoint(
|
|
1118
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
1119
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
1120
|
+
) {
|
|
836
1121
|
if (!request) {
|
|
837
1122
|
return;
|
|
838
1123
|
}
|
|
@@ -840,9 +1125,9 @@ export class SequencerPublisher {
|
|
|
840
1125
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
841
1126
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
842
1127
|
|
|
843
|
-
const { gasUsed,
|
|
844
|
-
const logData = { gasUsed,
|
|
845
|
-
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);
|
|
846
1131
|
this.addRequest({
|
|
847
1132
|
action: `invalidate-by-${request.reason}`,
|
|
848
1133
|
request: request.request,
|
|
@@ -855,9 +1140,9 @@ export class SequencerPublisher {
|
|
|
855
1140
|
result.receipt.status === 'success' &&
|
|
856
1141
|
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
857
1142
|
if (!success) {
|
|
858
|
-
this.log.warn(`Invalidate
|
|
1143
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
859
1144
|
} else {
|
|
860
|
-
this.log.info(`Invalidate
|
|
1145
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
861
1146
|
}
|
|
862
1147
|
return !!success;
|
|
863
1148
|
},
|
|
@@ -882,13 +1167,30 @@ export class SequencerPublisher {
|
|
|
882
1167
|
|
|
883
1168
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
884
1169
|
|
|
1170
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1171
|
+
|
|
885
1172
|
let gasUsed: bigint;
|
|
1173
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
886
1174
|
try {
|
|
887
|
-
({ 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
|
|
888
1176
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
889
1177
|
} catch (err) {
|
|
890
|
-
const viemError = formatViemError(err);
|
|
1178
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
891
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
|
+
|
|
892
1194
|
return false;
|
|
893
1195
|
}
|
|
894
1196
|
|
|
@@ -896,10 +1198,14 @@ export class SequencerPublisher {
|
|
|
896
1198
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
897
1199
|
logData.gasLimit = gasLimit;
|
|
898
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
|
+
|
|
899
1205
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
900
1206
|
this.addRequest({
|
|
901
1207
|
action,
|
|
902
|
-
request,
|
|
1208
|
+
request: requestWithAbi,
|
|
903
1209
|
gasConfig: { gasLimit },
|
|
904
1210
|
lastValidL2Slot: slotNumber,
|
|
905
1211
|
checkSuccess: (_req, result) => {
|
|
@@ -936,7 +1242,7 @@ export class SequencerPublisher {
|
|
|
936
1242
|
private async prepareProposeTx(
|
|
937
1243
|
encodedData: L1ProcessArgs,
|
|
938
1244
|
timestamp: bigint,
|
|
939
|
-
options: {
|
|
1245
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
940
1246
|
) {
|
|
941
1247
|
const kzg = Blob.getViemKzgInstance();
|
|
942
1248
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
@@ -968,9 +1274,27 @@ export class SequencerPublisher {
|
|
|
968
1274
|
kzg,
|
|
969
1275
|
},
|
|
970
1276
|
)
|
|
971
|
-
.catch(err => {
|
|
972
|
-
const
|
|
973
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
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({
|
|
1281
|
+
abi: RollupAbi,
|
|
1282
|
+
functionName: 'validateBlobs',
|
|
1283
|
+
args: [blobInput],
|
|
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
|
+
});
|
|
974
1298
|
throw new Error('Failed to validate blobs');
|
|
975
1299
|
});
|
|
976
1300
|
}
|
|
@@ -981,8 +1305,7 @@ export class SequencerPublisher {
|
|
|
981
1305
|
header: encodedData.header.toViem(),
|
|
982
1306
|
archive: toHex(encodedData.archive),
|
|
983
1307
|
oracleInput: {
|
|
984
|
-
|
|
985
|
-
feeAssetPriceModifier: 0n,
|
|
1308
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
986
1309
|
},
|
|
987
1310
|
},
|
|
988
1311
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1008,7 +1331,7 @@ export class SequencerPublisher {
|
|
|
1008
1331
|
readonly header: ViemHeader;
|
|
1009
1332
|
readonly archive: `0x${string}`;
|
|
1010
1333
|
readonly oracleInput: {
|
|
1011
|
-
readonly feeAssetPriceModifier:
|
|
1334
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1012
1335
|
};
|
|
1013
1336
|
},
|
|
1014
1337
|
ViemCommitteeAttestations,
|
|
@@ -1017,7 +1340,7 @@ export class SequencerPublisher {
|
|
|
1017
1340
|
`0x${string}`,
|
|
1018
1341
|
],
|
|
1019
1342
|
timestamp: bigint,
|
|
1020
|
-
options: {
|
|
1343
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1021
1344
|
) {
|
|
1022
1345
|
const rollupData = encodeFunctionData({
|
|
1023
1346
|
abi: RollupAbi,
|
|
@@ -1025,10 +1348,10 @@ export class SequencerPublisher {
|
|
|
1025
1348
|
args,
|
|
1026
1349
|
});
|
|
1027
1350
|
|
|
1028
|
-
// override the pending
|
|
1029
|
-
const
|
|
1030
|
-
options.
|
|
1031
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.
|
|
1351
|
+
// override the pending checkpoint number if requested
|
|
1352
|
+
const forcePendingCheckpointNumberStateDiff = (
|
|
1353
|
+
options.forcePendingCheckpointNumber !== undefined
|
|
1354
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1032
1355
|
: []
|
|
1033
1356
|
).flatMap(override => override.stateDiff ?? []);
|
|
1034
1357
|
|
|
@@ -1038,7 +1361,7 @@ export class SequencerPublisher {
|
|
|
1038
1361
|
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1039
1362
|
stateDiff: [
|
|
1040
1363
|
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1041
|
-
...
|
|
1364
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1042
1365
|
],
|
|
1043
1366
|
},
|
|
1044
1367
|
];
|
|
@@ -1050,25 +1373,27 @@ export class SequencerPublisher {
|
|
|
1050
1373
|
});
|
|
1051
1374
|
}
|
|
1052
1375
|
|
|
1376
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1377
|
+
|
|
1053
1378
|
const simulationResult = await this.l1TxUtils
|
|
1054
1379
|
.simulate(
|
|
1055
1380
|
{
|
|
1056
1381
|
to: this.rollupContract.address,
|
|
1057
1382
|
data: rollupData,
|
|
1058
|
-
gas:
|
|
1383
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1059
1384
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1060
1385
|
},
|
|
1061
1386
|
{
|
|
1062
1387
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1063
1388
|
time: timestamp + 1n,
|
|
1064
1389
|
// @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:
|
|
1390
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1066
1391
|
},
|
|
1067
1392
|
stateOverrides,
|
|
1068
1393
|
RollupAbi,
|
|
1069
1394
|
{
|
|
1070
1395
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1071
|
-
fallbackGasEstimate:
|
|
1396
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1072
1397
|
},
|
|
1073
1398
|
)
|
|
1074
1399
|
.catch(err => {
|
|
@@ -1078,11 +1403,23 @@ export class SequencerPublisher {
|
|
|
1078
1403
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1079
1404
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1080
1405
|
return {
|
|
1081
|
-
gasUsed:
|
|
1406
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1082
1407
|
logs: [],
|
|
1083
1408
|
};
|
|
1084
1409
|
}
|
|
1085
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
|
+
});
|
|
1086
1423
|
throw err;
|
|
1087
1424
|
});
|
|
1088
1425
|
|
|
@@ -1090,11 +1427,12 @@ export class SequencerPublisher {
|
|
|
1090
1427
|
}
|
|
1091
1428
|
|
|
1092
1429
|
private async addProposeTx(
|
|
1093
|
-
|
|
1430
|
+
checkpoint: Checkpoint,
|
|
1094
1431
|
encodedData: L1ProcessArgs,
|
|
1095
|
-
opts: { txTimeoutAt?: Date;
|
|
1432
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1096
1433
|
timestamp: bigint,
|
|
1097
1434
|
): Promise<void> {
|
|
1435
|
+
const slot = checkpoint.header.slotNumber;
|
|
1098
1436
|
const timer = new Timer();
|
|
1099
1437
|
const kzg = Blob.getViemKzgInstance();
|
|
1100
1438
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
@@ -1109,11 +1447,13 @@ export class SequencerPublisher {
|
|
|
1109
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
|
|
1110
1448
|
);
|
|
1111
1449
|
|
|
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
|
-
|
|
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
|
+
);
|
|
1117
1457
|
|
|
1118
1458
|
return this.addRequest({
|
|
1119
1459
|
action: 'propose',
|
|
@@ -1121,7 +1461,7 @@ export class SequencerPublisher {
|
|
|
1121
1461
|
to: this.rollupContract.address,
|
|
1122
1462
|
data: rollupData,
|
|
1123
1463
|
},
|
|
1124
|
-
lastValidL2Slot:
|
|
1464
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1125
1465
|
gasConfig: { ...opts, gasLimit },
|
|
1126
1466
|
blobConfig: {
|
|
1127
1467
|
blobs: encodedData.blobs.map(b => b.data),
|
|
@@ -1136,11 +1476,12 @@ export class SequencerPublisher {
|
|
|
1136
1476
|
receipt &&
|
|
1137
1477
|
receipt.status === 'success' &&
|
|
1138
1478
|
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1479
|
+
|
|
1139
1480
|
if (success) {
|
|
1140
1481
|
const endBlock = receipt.blockNumber;
|
|
1141
1482
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1142
1483
|
const { calldataGas, calldataSize, sender } = stats!;
|
|
1143
|
-
const publishStats:
|
|
1484
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1144
1485
|
gasPrice: receipt.effectiveGasPrice,
|
|
1145
1486
|
gasUsed: receipt.gasUsed,
|
|
1146
1487
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
@@ -1149,23 +1490,26 @@ export class SequencerPublisher {
|
|
|
1149
1490
|
calldataGas,
|
|
1150
1491
|
calldataSize,
|
|
1151
1492
|
sender,
|
|
1152
|
-
...
|
|
1493
|
+
...checkpoint.getStats(),
|
|
1153
1494
|
eventName: 'rollup-published-to-l1',
|
|
1154
1495
|
blobCount: encodedData.blobs.length,
|
|
1155
1496
|
inclusionBlocks,
|
|
1156
1497
|
};
|
|
1157
|
-
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
|
+
});
|
|
1158
1503
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1159
1504
|
|
|
1160
1505
|
return true;
|
|
1161
1506
|
} else {
|
|
1162
1507
|
this.metrics.recordFailedTx('process');
|
|
1163
|
-
this.log.error(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
});
|
|
1508
|
+
this.log.error(
|
|
1509
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1510
|
+
undefined,
|
|
1511
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1512
|
+
);
|
|
1169
1513
|
return false;
|
|
1170
1514
|
}
|
|
1171
1515
|
},
|