@aztec/sequencer-client 0.0.1-commit.24de95ac → 0.0.1-commit.2e2504e2
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 +12 -12
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +33 -25
- package/dest/config.d.ts +12 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +80 -28
- package/dest/global_variable_builder/global_builder.d.ts +22 -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 +9 -4
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +14 -3
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +5 -4
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +1 -1
- 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 +75 -62
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +694 -170
- 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 +1191 -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 +32 -3
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +165 -54
- package/dest/sequencer/sequencer.d.ts +112 -142
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +721 -505
- package/dest/sequencer/timetable.d.ts +54 -14
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +148 -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 +4 -3
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +95 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +220 -0
- package/dest/test/utils.d.ts +53 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +103 -0
- package/package.json +33 -30
- package/src/client/sequencer-client.ts +31 -42
- package/src/config.ts +86 -32
- package/src/global_variable_builder/global_builder.ts +67 -59
- package/src/index.ts +1 -7
- package/src/publisher/config.ts +20 -9
- package/src/publisher/sequencer-publisher-factory.ts +7 -5
- package/src/publisher/sequencer-publisher-metrics.ts +19 -71
- package/src/publisher/sequencer-publisher.ts +411 -217
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/checkpoint_proposal_job.ts +877 -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 +222 -61
- package/src/sequencer/sequencer.ts +472 -697
- package/src/sequencer/timetable.ts +173 -79
- package/src/sequencer/types.ts +6 -0
- package/src/sequencer/utils.ts +18 -9
- package/src/test/index.ts +3 -2
- package/src/test/mock_checkpoint_builder.ts +309 -0
- package/src/test/utils.ts +164 -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,46 +1,49 @@
|
|
|
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
|
-
FormattedViemError,
|
|
8
7
|
type GovernanceProposerContract,
|
|
9
8
|
type IEmpireBase,
|
|
10
|
-
type L1BlobInputs,
|
|
11
|
-
type L1ContractsConfig,
|
|
12
|
-
type L1TxConfig,
|
|
13
|
-
type L1TxRequest,
|
|
14
9
|
MULTI_CALL_3_ADDRESS,
|
|
15
10
|
Multicall3,
|
|
16
11
|
RollupContract,
|
|
17
12
|
type TallySlashingProposerContract,
|
|
18
|
-
type TransactionStats,
|
|
19
13
|
type ViemCommitteeAttestations,
|
|
20
14
|
type ViemHeader,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
} from '@aztec/ethereum/contracts';
|
|
16
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
|
+
import {
|
|
18
|
+
type L1BlobInputs,
|
|
19
|
+
type L1TxConfig,
|
|
20
|
+
type L1TxRequest,
|
|
21
|
+
MAX_L1_TX_LIMIT,
|
|
22
|
+
type TransactionStats,
|
|
23
|
+
WEI_CONST,
|
|
24
|
+
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
25
|
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
27
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
28
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
29
|
+
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
30
|
+
import { pick } from '@aztec/foundation/collection';
|
|
31
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
28
32
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
29
33
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
30
|
-
import type { Fr } from '@aztec/foundation/fields';
|
|
31
34
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
32
35
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
33
36
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
34
37
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
35
38
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
36
|
-
import {
|
|
39
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
40
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
37
41
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
38
42
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
39
|
-
import type {
|
|
40
|
-
import {
|
|
41
|
-
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
43
|
+
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
42
45
|
|
|
43
|
-
import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
46
|
+
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
44
47
|
|
|
45
48
|
import type { PublisherConfig, TxSenderConfig } from './config.js';
|
|
46
49
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
@@ -51,8 +54,6 @@ type L1ProcessArgs = {
|
|
|
51
54
|
header: CheckpointHeader;
|
|
52
55
|
/** A root of the archive tree after the L2 block is applied. */
|
|
53
56
|
archive: Buffer;
|
|
54
|
-
/** State reference after the L2 block is applied. */
|
|
55
|
-
stateReference: StateReference;
|
|
56
57
|
/** L2 block blobs containing all tx effects. */
|
|
57
58
|
blobs: Blob[];
|
|
58
59
|
/** Attestations */
|
|
@@ -80,18 +81,18 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
|
|
|
80
81
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
81
82
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
82
83
|
|
|
83
|
-
export type
|
|
84
|
+
export type InvalidateCheckpointRequest = {
|
|
84
85
|
request: L1TxRequest;
|
|
85
86
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
86
87
|
gasUsed: bigint;
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
checkpointNumber: CheckpointNumber;
|
|
89
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
interface RequestWithExpiry {
|
|
92
93
|
action: Action;
|
|
93
94
|
request: L1TxRequest;
|
|
94
|
-
lastValidL2Slot:
|
|
95
|
+
lastValidL2Slot: SlotNumber;
|
|
95
96
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
96
97
|
blobConfig?: L1BlobInputs;
|
|
97
98
|
checkSuccess: (
|
|
@@ -108,17 +109,20 @@ export class SequencerPublisher {
|
|
|
108
109
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
109
110
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
110
111
|
|
|
111
|
-
protected lastActions: Partial<Record<Action,
|
|
112
|
+
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
|
+
|
|
114
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
112
115
|
|
|
113
116
|
protected log: Logger;
|
|
114
117
|
protected ethereumSlotDuration: bigint;
|
|
115
118
|
|
|
116
|
-
private
|
|
117
|
-
// @note - with blobs, the below estimate seems too large.
|
|
118
|
-
// Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
|
|
119
|
-
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
120
|
-
public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
|
|
119
|
+
private blobClient: BlobClientInterface;
|
|
121
120
|
|
|
121
|
+
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
122
|
+
private proposerAddressForSimulation?: EthAddress;
|
|
123
|
+
|
|
124
|
+
/** L1 fee analyzer for fisherman mode */
|
|
125
|
+
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
122
126
|
// A CALL to a cold address is 2700 gas
|
|
123
127
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
124
128
|
|
|
@@ -131,13 +135,15 @@ export class SequencerPublisher {
|
|
|
131
135
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
132
136
|
public slashFactoryContract: SlashFactoryContract;
|
|
133
137
|
|
|
138
|
+
public readonly tracer: Tracer;
|
|
139
|
+
|
|
134
140
|
protected requests: RequestWithExpiry[] = [];
|
|
135
141
|
|
|
136
142
|
constructor(
|
|
137
143
|
private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
|
|
138
144
|
deps: {
|
|
139
145
|
telemetry?: TelemetryClient;
|
|
140
|
-
|
|
146
|
+
blobClient: BlobClientInterface;
|
|
141
147
|
l1TxUtils: L1TxUtilsWithBlobs;
|
|
142
148
|
rollupContract: RollupContract;
|
|
143
149
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -146,7 +152,7 @@ export class SequencerPublisher {
|
|
|
146
152
|
epochCache: EpochCache;
|
|
147
153
|
dateProvider: DateProvider;
|
|
148
154
|
metrics: SequencerPublisherMetrics;
|
|
149
|
-
lastActions: Partial<Record<Action,
|
|
155
|
+
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
150
156
|
log?: Logger;
|
|
151
157
|
},
|
|
152
158
|
) {
|
|
@@ -155,11 +161,11 @@ export class SequencerPublisher {
|
|
|
155
161
|
this.epochCache = deps.epochCache;
|
|
156
162
|
this.lastActions = deps.lastActions;
|
|
157
163
|
|
|
158
|
-
this.
|
|
159
|
-
deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
|
|
164
|
+
this.blobClient = deps.blobClient;
|
|
160
165
|
|
|
161
166
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
162
167
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
168
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
163
169
|
this.l1TxUtils = deps.l1TxUtils;
|
|
164
170
|
|
|
165
171
|
this.rollupContract = deps.rollupContract;
|
|
@@ -173,6 +179,15 @@ export class SequencerPublisher {
|
|
|
173
179
|
this.slashingProposerContract = newSlashingProposer;
|
|
174
180
|
});
|
|
175
181
|
this.slashFactoryContract = deps.slashFactoryContract;
|
|
182
|
+
|
|
183
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
184
|
+
if (config.fishermanMode) {
|
|
185
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
186
|
+
this.l1TxUtils.client,
|
|
187
|
+
deps.dateProvider,
|
|
188
|
+
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
176
191
|
}
|
|
177
192
|
|
|
178
193
|
public getRollupContract(): RollupContract {
|
|
@@ -183,14 +198,96 @@ export class SequencerPublisher {
|
|
|
183
198
|
return this.l1TxUtils.getSenderAddress();
|
|
184
199
|
}
|
|
185
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
203
|
+
*/
|
|
204
|
+
public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
|
|
205
|
+
return this.l1FeeAnalyzer;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sets the proposer address to use for simulations in fisherman mode.
|
|
210
|
+
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
211
|
+
*/
|
|
212
|
+
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
213
|
+
this.proposerAddressForSimulation = proposerAddress;
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
public addRequest(request: RequestWithExpiry) {
|
|
187
217
|
this.requests.push(request);
|
|
188
218
|
}
|
|
189
219
|
|
|
190
|
-
public getCurrentL2Slot():
|
|
220
|
+
public getCurrentL2Slot(): SlotNumber {
|
|
191
221
|
return this.epochCache.getEpochAndSlotNow().slot;
|
|
192
222
|
}
|
|
193
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Clears all pending requests without sending them.
|
|
226
|
+
*/
|
|
227
|
+
public clearPendingRequests(): void {
|
|
228
|
+
const count = this.requests.length;
|
|
229
|
+
this.requests = [];
|
|
230
|
+
if (count > 0) {
|
|
231
|
+
this.log.debug(`Cleared ${count} pending request(s)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
237
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
238
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
239
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
240
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
241
|
+
*/
|
|
242
|
+
public async analyzeL1Fees(
|
|
243
|
+
l2SlotNumber: SlotNumber,
|
|
244
|
+
onComplete?: (analysis: L1FeeAnalysisResult) => void,
|
|
245
|
+
): Promise<L1FeeAnalysisResult | undefined> {
|
|
246
|
+
if (!this.l1FeeAnalyzer) {
|
|
247
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const requestsToAnalyze = [...this.requests];
|
|
252
|
+
if (requestsToAnalyze.length === 0) {
|
|
253
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Extract blob config from requests (if any)
|
|
258
|
+
const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
259
|
+
const blobConfig = blobConfigs[0];
|
|
260
|
+
|
|
261
|
+
// Get gas configs
|
|
262
|
+
const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
263
|
+
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
264
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
|
|
265
|
+
|
|
266
|
+
// Get the transaction requests
|
|
267
|
+
const l1Requests = requestsToAnalyze.map(r => r.request);
|
|
268
|
+
|
|
269
|
+
// Start the analysis
|
|
270
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
271
|
+
l2SlotNumber,
|
|
272
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
273
|
+
l1Requests,
|
|
274
|
+
blobConfig,
|
|
275
|
+
onComplete,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
this.log.info('Started L1 fee analysis', {
|
|
279
|
+
analysisId,
|
|
280
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
281
|
+
requestCount: requestsToAnalyze.length,
|
|
282
|
+
hasBlobConfig: !!blobConfig,
|
|
283
|
+
gasLimit: gasLimit.toString(),
|
|
284
|
+
actions: requestsToAnalyze.map(r => r.action),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
288
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
289
|
+
}
|
|
290
|
+
|
|
194
291
|
/**
|
|
195
292
|
* Sends all requests that are still valid.
|
|
196
293
|
* @returns one of:
|
|
@@ -198,10 +295,11 @@ export class SequencerPublisher {
|
|
|
198
295
|
* - a receipt and errorMsg if it failed on L1
|
|
199
296
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
200
297
|
*/
|
|
298
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
201
299
|
public async sendRequests() {
|
|
202
300
|
const requestsToProcess = [...this.requests];
|
|
203
301
|
this.requests = [];
|
|
204
|
-
if (this.interrupted) {
|
|
302
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
205
303
|
return undefined;
|
|
206
304
|
}
|
|
207
305
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -244,7 +342,16 @@ export class SequencerPublisher {
|
|
|
244
342
|
|
|
245
343
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
246
344
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
247
|
-
|
|
345
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
346
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
347
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
348
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
349
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
350
|
+
requested: gasLimit,
|
|
351
|
+
capped: maxGas,
|
|
352
|
+
});
|
|
353
|
+
gasLimit = maxGas;
|
|
354
|
+
}
|
|
248
355
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
249
356
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
250
357
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -315,13 +422,15 @@ export class SequencerPublisher {
|
|
|
315
422
|
public canProposeAtNextEthBlock(
|
|
316
423
|
tipArchive: Fr,
|
|
317
424
|
msgSender: EthAddress,
|
|
318
|
-
opts: {
|
|
425
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
319
426
|
) {
|
|
320
427
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
321
428
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
322
429
|
|
|
323
430
|
return this.rollupContract
|
|
324
|
-
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration,
|
|
431
|
+
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
432
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
433
|
+
})
|
|
325
434
|
.catch(err => {
|
|
326
435
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
327
436
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -339,7 +448,11 @@ export class SequencerPublisher {
|
|
|
339
448
|
* It will throw if the block header is invalid.
|
|
340
449
|
* @param header - The block header to validate
|
|
341
450
|
*/
|
|
342
|
-
|
|
451
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
452
|
+
public async validateBlockHeader(
|
|
453
|
+
header: CheckpointHeader,
|
|
454
|
+
opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
|
|
455
|
+
): Promise<void> {
|
|
343
456
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
344
457
|
|
|
345
458
|
const args = [
|
|
@@ -348,15 +461,27 @@ export class SequencerPublisher {
|
|
|
348
461
|
[], // no signers
|
|
349
462
|
Signature.empty().toViemSignature(),
|
|
350
463
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
351
|
-
header.
|
|
464
|
+
header.blobsHash.toString(),
|
|
352
465
|
flags,
|
|
353
466
|
] as const;
|
|
354
467
|
|
|
355
468
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
469
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
470
|
+
opts?.forcePendingCheckpointNumber,
|
|
471
|
+
);
|
|
472
|
+
let balance = 0n;
|
|
473
|
+
if (this.config.fishermanMode) {
|
|
474
|
+
// In fisherman mode, we can't know where the proposer is publishing from
|
|
475
|
+
// so we just add sufficient balance to the multicall3 address
|
|
476
|
+
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
477
|
+
} else {
|
|
478
|
+
balance = await this.l1TxUtils.getSenderBalance();
|
|
479
|
+
}
|
|
480
|
+
stateOverrides.push({
|
|
481
|
+
address: MULTI_CALL_3_ADDRESS,
|
|
482
|
+
balance,
|
|
483
|
+
});
|
|
356
484
|
|
|
357
|
-
// use sender balance to simulate
|
|
358
|
-
const balance = await this.l1TxUtils.getSenderBalance();
|
|
359
|
-
this.log.debug(`Simulating validateHeader with balance: ${balance}`);
|
|
360
485
|
await this.l1TxUtils.simulate(
|
|
361
486
|
{
|
|
362
487
|
to: this.rollupContract.address,
|
|
@@ -364,86 +489,101 @@ export class SequencerPublisher {
|
|
|
364
489
|
from: MULTI_CALL_3_ADDRESS,
|
|
365
490
|
},
|
|
366
491
|
{ time: ts + 1n },
|
|
367
|
-
|
|
368
|
-
{ address: MULTI_CALL_3_ADDRESS, balance },
|
|
369
|
-
...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
|
|
370
|
-
],
|
|
492
|
+
stateOverrides,
|
|
371
493
|
);
|
|
372
494
|
this.log.debug(`Simulated validateHeader`);
|
|
373
495
|
}
|
|
374
496
|
|
|
375
497
|
/**
|
|
376
|
-
* Simulate making a call to invalidate a
|
|
377
|
-
* @param
|
|
498
|
+
* Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
|
|
499
|
+
* @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
|
|
378
500
|
*/
|
|
379
|
-
public async
|
|
380
|
-
validationResult:
|
|
381
|
-
): Promise<
|
|
501
|
+
public async simulateInvalidateCheckpoint(
|
|
502
|
+
validationResult: ValidateCheckpointResult,
|
|
503
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
382
504
|
if (validationResult.valid) {
|
|
383
505
|
return undefined;
|
|
384
506
|
}
|
|
385
507
|
|
|
386
|
-
const { reason,
|
|
387
|
-
const
|
|
388
|
-
const logData = { ...
|
|
508
|
+
const { reason, checkpoint } = validationResult;
|
|
509
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
510
|
+
const logData = { ...checkpoint, reason };
|
|
389
511
|
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
512
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
513
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
392
514
|
this.log.verbose(
|
|
393
|
-
`Skipping
|
|
394
|
-
{
|
|
515
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
516
|
+
{ currentCheckpointNumber, ...logData },
|
|
395
517
|
);
|
|
396
518
|
return undefined;
|
|
397
519
|
}
|
|
398
520
|
|
|
399
|
-
const request = this.
|
|
400
|
-
this.log.debug(`Simulating invalidate
|
|
521
|
+
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
|
+
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
401
523
|
|
|
402
524
|
try {
|
|
403
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
404
|
-
|
|
525
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
|
+
request,
|
|
527
|
+
undefined,
|
|
528
|
+
undefined,
|
|
529
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
530
|
+
);
|
|
531
|
+
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
532
|
+
...logData,
|
|
533
|
+
request,
|
|
534
|
+
gasUsed,
|
|
535
|
+
});
|
|
405
536
|
|
|
406
|
-
return {
|
|
537
|
+
return {
|
|
538
|
+
request,
|
|
539
|
+
gasUsed,
|
|
540
|
+
checkpointNumber,
|
|
541
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
542
|
+
reason,
|
|
543
|
+
};
|
|
407
544
|
} catch (err) {
|
|
408
545
|
const viemError = formatViemError(err);
|
|
409
546
|
|
|
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('
|
|
547
|
+
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
548
|
+
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
549
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
413
550
|
this.log.verbose(
|
|
414
|
-
`Simulation for invalidate
|
|
551
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
415
552
|
{ ...logData, request, error: viemError.message },
|
|
416
553
|
);
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
this.log.verbose(`
|
|
554
|
+
const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
555
|
+
if (latestPendingCheckpointNumber < checkpointNumber) {
|
|
556
|
+
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
420
557
|
return undefined;
|
|
421
558
|
} else {
|
|
422
559
|
this.log.error(
|
|
423
|
-
`Simulation for invalidate ${
|
|
560
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
424
561
|
viemError,
|
|
425
562
|
logData,
|
|
426
563
|
);
|
|
427
|
-
throw new Error(
|
|
428
|
-
|
|
429
|
-
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
566
|
+
{
|
|
567
|
+
cause: viemError,
|
|
568
|
+
},
|
|
569
|
+
);
|
|
430
570
|
}
|
|
431
571
|
}
|
|
432
572
|
|
|
433
|
-
// Otherwise, throw. We cannot build the next
|
|
434
|
-
this.log.error(`Simulation for invalidate
|
|
435
|
-
throw new Error(`Failed to simulate invalidate
|
|
573
|
+
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
|
+
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
575
|
+
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
436
576
|
}
|
|
437
577
|
}
|
|
438
578
|
|
|
439
|
-
private
|
|
579
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
440
580
|
if (validationResult.valid) {
|
|
441
|
-
throw new Error('Cannot invalidate a valid
|
|
581
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
442
582
|
}
|
|
443
583
|
|
|
444
|
-
const {
|
|
445
|
-
const logData = { ...
|
|
446
|
-
this.log.debug(`
|
|
584
|
+
const { checkpoint, committee, reason } = validationResult;
|
|
585
|
+
const logData = { ...checkpoint, reason };
|
|
586
|
+
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
447
587
|
|
|
448
588
|
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
449
589
|
validationResult.attestations,
|
|
@@ -451,14 +591,14 @@ export class SequencerPublisher {
|
|
|
451
591
|
|
|
452
592
|
if (reason === 'invalid-attestation') {
|
|
453
593
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
454
|
-
|
|
594
|
+
checkpoint.checkpointNumber,
|
|
455
595
|
attestationsAndSigners,
|
|
456
596
|
committee,
|
|
457
597
|
validationResult.invalidIndex,
|
|
458
598
|
);
|
|
459
599
|
} else if (reason === 'insufficient-attestations') {
|
|
460
600
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
461
|
-
|
|
601
|
+
checkpoint.checkpointNumber,
|
|
462
602
|
attestationsAndSigners,
|
|
463
603
|
committee,
|
|
464
604
|
);
|
|
@@ -468,46 +608,39 @@ export class SequencerPublisher {
|
|
|
468
608
|
}
|
|
469
609
|
}
|
|
470
610
|
|
|
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,
|
|
611
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
612
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
613
|
+
public async validateCheckpointForSubmission(
|
|
614
|
+
checkpoint: Checkpoint,
|
|
482
615
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
483
616
|
attestationsAndSignersSignature: Signature,
|
|
484
|
-
options: {
|
|
617
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
485
618
|
): Promise<bigint> {
|
|
486
619
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
487
620
|
|
|
621
|
+
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
488
622
|
// If we have no attestations, we still need to provide the empty attestations
|
|
489
623
|
// so that the committee is recalculated correctly
|
|
490
|
-
const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
491
|
-
if (ignoreSignatures) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const blobFields =
|
|
624
|
+
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
625
|
+
// if (ignoreSignatures) {
|
|
626
|
+
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
627
|
+
// if (!committee) {
|
|
628
|
+
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
629
|
+
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
630
|
+
// }
|
|
631
|
+
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
632
|
+
// CommitteeAttestation.fromAddress(committeeMember),
|
|
633
|
+
// );
|
|
634
|
+
// }
|
|
635
|
+
|
|
636
|
+
const blobFields = checkpoint.toBlobFields();
|
|
503
637
|
const blobs = getBlobsPerL1Block(blobFields);
|
|
504
638
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
505
639
|
|
|
506
640
|
const args = [
|
|
507
641
|
{
|
|
508
|
-
header:
|
|
509
|
-
archive: toHex(
|
|
510
|
-
stateReference: block.header.state.toViem(),
|
|
642
|
+
header: checkpoint.header.toViem(),
|
|
643
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
511
644
|
oracleInput: {
|
|
512
645
|
feeAssetPriceModifier: 0n,
|
|
513
646
|
},
|
|
@@ -523,7 +656,7 @@ export class SequencerPublisher {
|
|
|
523
656
|
}
|
|
524
657
|
|
|
525
658
|
private async enqueueCastSignalHelper(
|
|
526
|
-
slotNumber:
|
|
659
|
+
slotNumber: SlotNumber,
|
|
527
660
|
timestamp: bigint,
|
|
528
661
|
signalType: GovernanceSignalAction,
|
|
529
662
|
payload: EthAddress,
|
|
@@ -545,10 +678,19 @@ export class SequencerPublisher {
|
|
|
545
678
|
const round = await base.computeRound(slotNumber);
|
|
546
679
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
547
680
|
|
|
681
|
+
if (roundInfo.quorumReached) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
|
|
548
685
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
549
686
|
return false;
|
|
550
687
|
}
|
|
551
688
|
|
|
689
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
690
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
|
|
552
694
|
const cachedLastVote = this.lastActions[signalType];
|
|
553
695
|
this.lastActions[signalType] = slotNumber;
|
|
554
696
|
const action = signalType;
|
|
@@ -568,7 +710,7 @@ export class SequencerPublisher {
|
|
|
568
710
|
});
|
|
569
711
|
|
|
570
712
|
try {
|
|
571
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
713
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
572
714
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
573
715
|
} catch (err) {
|
|
574
716
|
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
|
|
@@ -591,14 +733,14 @@ export class SequencerPublisher {
|
|
|
591
733
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
592
734
|
if (!success) {
|
|
593
735
|
this.log.error(
|
|
594
|
-
`Signaling in
|
|
736
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
595
737
|
logData,
|
|
596
738
|
);
|
|
597
739
|
this.lastActions[signalType] = cachedLastVote;
|
|
598
740
|
return false;
|
|
599
741
|
} else {
|
|
600
742
|
this.log.info(
|
|
601
|
-
`Signaling in
|
|
743
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
602
744
|
logData,
|
|
603
745
|
);
|
|
604
746
|
return true;
|
|
@@ -608,6 +750,17 @@ export class SequencerPublisher {
|
|
|
608
750
|
return true;
|
|
609
751
|
}
|
|
610
752
|
|
|
753
|
+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
754
|
+
const key = payload.toString();
|
|
755
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
756
|
+
if (cached) {
|
|
757
|
+
return cached;
|
|
758
|
+
}
|
|
759
|
+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
760
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
761
|
+
return isEmpty;
|
|
762
|
+
}
|
|
763
|
+
|
|
611
764
|
/**
|
|
612
765
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
613
766
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -616,7 +769,7 @@ export class SequencerPublisher {
|
|
|
616
769
|
*/
|
|
617
770
|
public enqueueGovernanceCastSignal(
|
|
618
771
|
governancePayload: EthAddress,
|
|
619
|
-
slotNumber:
|
|
772
|
+
slotNumber: SlotNumber,
|
|
620
773
|
timestamp: bigint,
|
|
621
774
|
signerAddress: EthAddress,
|
|
622
775
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
@@ -635,7 +788,7 @@ export class SequencerPublisher {
|
|
|
635
788
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
636
789
|
public async enqueueSlashingActions(
|
|
637
790
|
actions: ProposerSlashAction[],
|
|
638
|
-
slotNumber:
|
|
791
|
+
slotNumber: SlotNumber,
|
|
639
792
|
timestamp: bigint,
|
|
640
793
|
signerAddress: EthAddress,
|
|
641
794
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
@@ -755,28 +908,21 @@ export class SequencerPublisher {
|
|
|
755
908
|
return true;
|
|
756
909
|
}
|
|
757
910
|
|
|
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,
|
|
911
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
912
|
+
public async enqueueProposeCheckpoint(
|
|
913
|
+
checkpoint: Checkpoint,
|
|
766
914
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
767
915
|
attestationsAndSignersSignature: Signature,
|
|
768
|
-
opts: { txTimeoutAt?: Date;
|
|
769
|
-
): Promise<
|
|
770
|
-
const checkpointHeader =
|
|
916
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
917
|
+
): Promise<void> {
|
|
918
|
+
const checkpointHeader = checkpoint.header;
|
|
771
919
|
|
|
772
|
-
const blobFields =
|
|
920
|
+
const blobFields = checkpoint.toBlobFields();
|
|
773
921
|
const blobs = getBlobsPerL1Block(blobFields);
|
|
774
922
|
|
|
775
923
|
const proposeTxArgs = {
|
|
776
924
|
header: checkpointHeader,
|
|
777
|
-
archive:
|
|
778
|
-
stateReference: block.header.state,
|
|
779
|
-
body: block.body.toBuffer(),
|
|
925
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
780
926
|
blobs,
|
|
781
927
|
attestationsAndSigners,
|
|
782
928
|
attestationsAndSignersSignature,
|
|
@@ -790,22 +936,29 @@ export class SequencerPublisher {
|
|
|
790
936
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
791
937
|
// make time consistency checks break.
|
|
792
938
|
// 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.
|
|
939
|
+
ts = await this.validateCheckpointForSubmission(
|
|
940
|
+
checkpoint,
|
|
941
|
+
attestationsAndSigners,
|
|
942
|
+
attestationsAndSignersSignature,
|
|
943
|
+
opts,
|
|
944
|
+
);
|
|
794
945
|
} catch (err: any) {
|
|
795
|
-
this.log.error(`
|
|
796
|
-
...
|
|
797
|
-
slotNumber:
|
|
798
|
-
|
|
946
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
947
|
+
...checkpoint.getStats(),
|
|
948
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
949
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
799
950
|
});
|
|
800
951
|
throw err;
|
|
801
952
|
}
|
|
802
953
|
|
|
803
|
-
this.log.verbose(`Enqueuing
|
|
804
|
-
await this.addProposeTx(
|
|
805
|
-
return true;
|
|
954
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
955
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
806
956
|
}
|
|
807
957
|
|
|
808
|
-
public
|
|
958
|
+
public enqueueInvalidateCheckpoint(
|
|
959
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
960
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
961
|
+
) {
|
|
809
962
|
if (!request) {
|
|
810
963
|
return;
|
|
811
964
|
}
|
|
@@ -813,24 +966,24 @@ export class SequencerPublisher {
|
|
|
813
966
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
814
967
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
815
968
|
|
|
816
|
-
const { gasUsed,
|
|
817
|
-
const logData = { gasUsed,
|
|
818
|
-
this.log.verbose(`Enqueuing invalidate
|
|
969
|
+
const { gasUsed, checkpointNumber } = request;
|
|
970
|
+
const logData = { gasUsed, checkpointNumber, gasLimit, opts };
|
|
971
|
+
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
819
972
|
this.addRequest({
|
|
820
973
|
action: `invalidate-by-${request.reason}`,
|
|
821
974
|
request: request.request,
|
|
822
975
|
gasConfig: { gasLimit, txTimeoutAt: opts.txTimeoutAt },
|
|
823
|
-
lastValidL2Slot: this.getCurrentL2Slot() +
|
|
976
|
+
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
824
977
|
checkSuccess: (_req, result) => {
|
|
825
978
|
const success =
|
|
826
979
|
result &&
|
|
827
980
|
result.receipt &&
|
|
828
981
|
result.receipt.status === 'success' &&
|
|
829
|
-
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
982
|
+
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
830
983
|
if (!success) {
|
|
831
|
-
this.log.warn(`Invalidate
|
|
984
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
832
985
|
} else {
|
|
833
|
-
this.log.info(`Invalidate
|
|
986
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
834
987
|
}
|
|
835
988
|
return !!success;
|
|
836
989
|
},
|
|
@@ -841,7 +994,7 @@ export class SequencerPublisher {
|
|
|
841
994
|
action: Action,
|
|
842
995
|
request: L1TxRequest,
|
|
843
996
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
844
|
-
slotNumber:
|
|
997
|
+
slotNumber: SlotNumber,
|
|
845
998
|
timestamp: bigint,
|
|
846
999
|
) {
|
|
847
1000
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
@@ -856,12 +1009,14 @@ export class SequencerPublisher {
|
|
|
856
1009
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
857
1010
|
|
|
858
1011
|
let gasUsed: bigint;
|
|
1012
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
859
1013
|
try {
|
|
860
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1014
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
861
1015
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
862
1016
|
} catch (err) {
|
|
863
|
-
const viemError = formatViemError(err);
|
|
1017
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
864
1018
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
|
+
|
|
865
1020
|
return false;
|
|
866
1021
|
}
|
|
867
1022
|
|
|
@@ -869,10 +1024,14 @@ export class SequencerPublisher {
|
|
|
869
1024
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
870
1025
|
logData.gasLimit = gasLimit;
|
|
871
1026
|
|
|
1027
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1028
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1029
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1030
|
+
|
|
872
1031
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
873
1032
|
this.addRequest({
|
|
874
1033
|
action,
|
|
875
|
-
request,
|
|
1034
|
+
request: requestWithAbi,
|
|
876
1035
|
gasConfig: { gasLimit },
|
|
877
1036
|
lastValidL2Slot: slotNumber,
|
|
878
1037
|
checkSuccess: (_req, result) => {
|
|
@@ -909,41 +1068,50 @@ export class SequencerPublisher {
|
|
|
909
1068
|
private async prepareProposeTx(
|
|
910
1069
|
encodedData: L1ProcessArgs,
|
|
911
1070
|
timestamp: bigint,
|
|
912
|
-
options: {
|
|
1071
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
913
1072
|
) {
|
|
914
1073
|
const kzg = Blob.getViemKzgInstance();
|
|
915
1074
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
916
1075
|
this.log.debug('Validating blob input', { blobInput });
|
|
917
|
-
const blobEvaluationGas = await this.l1TxUtils
|
|
918
|
-
.estimateGas(
|
|
919
|
-
this.getSenderAddress().toString(),
|
|
920
|
-
{
|
|
921
|
-
to: this.rollupContract.address,
|
|
922
|
-
data: encodeFunctionData({
|
|
923
|
-
abi: RollupAbi,
|
|
924
|
-
functionName: 'validateBlobs',
|
|
925
|
-
args: [blobInput],
|
|
926
|
-
}),
|
|
927
|
-
},
|
|
928
|
-
{},
|
|
929
|
-
{
|
|
930
|
-
blobs: encodedData.blobs.map(b => b.data),
|
|
931
|
-
kzg,
|
|
932
|
-
},
|
|
933
|
-
)
|
|
934
|
-
.catch(err => {
|
|
935
|
-
const { message, metaMessages } = formatViemError(err);
|
|
936
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
937
|
-
throw new Error('Failed to validate blobs');
|
|
938
|
-
});
|
|
939
1076
|
|
|
1077
|
+
// Get blob evaluation gas
|
|
1078
|
+
let blobEvaluationGas: bigint;
|
|
1079
|
+
if (this.config.fishermanMode) {
|
|
1080
|
+
// In fisherman mode, we can't estimate blob gas because estimateGas doesn't support state overrides
|
|
1081
|
+
// Use a fixed estimate.
|
|
1082
|
+
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
1083
|
+
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
1084
|
+
} else {
|
|
1085
|
+
// Normal mode - use estimateGas with blob inputs
|
|
1086
|
+
blobEvaluationGas = await this.l1TxUtils
|
|
1087
|
+
.estimateGas(
|
|
1088
|
+
this.getSenderAddress().toString(),
|
|
1089
|
+
{
|
|
1090
|
+
to: this.rollupContract.address,
|
|
1091
|
+
data: encodeFunctionData({
|
|
1092
|
+
abi: RollupAbi,
|
|
1093
|
+
functionName: 'validateBlobs',
|
|
1094
|
+
args: [blobInput],
|
|
1095
|
+
}),
|
|
1096
|
+
},
|
|
1097
|
+
{},
|
|
1098
|
+
{
|
|
1099
|
+
blobs: encodedData.blobs.map(b => b.data),
|
|
1100
|
+
kzg,
|
|
1101
|
+
},
|
|
1102
|
+
)
|
|
1103
|
+
.catch(err => {
|
|
1104
|
+
const { message, metaMessages } = formatViemError(err);
|
|
1105
|
+
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1106
|
+
throw new Error('Failed to validate blobs');
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
940
1109
|
const signers = encodedData.attestationsAndSigners.getSigners().map(signer => signer.toString());
|
|
941
1110
|
|
|
942
1111
|
const args = [
|
|
943
1112
|
{
|
|
944
1113
|
header: encodedData.header.toViem(),
|
|
945
1114
|
archive: toHex(encodedData.archive),
|
|
946
|
-
stateReference: encodedData.stateReference.toViem(),
|
|
947
1115
|
oracleInput: {
|
|
948
1116
|
// We are currently not modifying these. See #9963
|
|
949
1117
|
feeAssetPriceModifier: 0n,
|
|
@@ -971,7 +1139,6 @@ export class SequencerPublisher {
|
|
|
971
1139
|
{
|
|
972
1140
|
readonly header: ViemHeader;
|
|
973
1141
|
readonly archive: `0x${string}`;
|
|
974
|
-
readonly stateReference: ViemStateReference;
|
|
975
1142
|
readonly oracleInput: {
|
|
976
1143
|
readonly feeAssetPriceModifier: 0n;
|
|
977
1144
|
};
|
|
@@ -982,7 +1149,7 @@ export class SequencerPublisher {
|
|
|
982
1149
|
`0x${string}`,
|
|
983
1150
|
],
|
|
984
1151
|
timestamp: bigint,
|
|
985
|
-
options: {
|
|
1152
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
986
1153
|
) {
|
|
987
1154
|
const rollupData = encodeFunctionData({
|
|
988
1155
|
abi: RollupAbi,
|
|
@@ -990,44 +1157,64 @@ export class SequencerPublisher {
|
|
|
990
1157
|
args,
|
|
991
1158
|
});
|
|
992
1159
|
|
|
993
|
-
// override the pending
|
|
994
|
-
const
|
|
995
|
-
options.
|
|
996
|
-
? await this.rollupContract.
|
|
1160
|
+
// override the pending checkpoint number if requested
|
|
1161
|
+
const forcePendingCheckpointNumberStateDiff = (
|
|
1162
|
+
options.forcePendingCheckpointNumber !== undefined
|
|
1163
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
997
1164
|
: []
|
|
998
1165
|
).flatMap(override => override.stateDiff ?? []);
|
|
999
1166
|
|
|
1167
|
+
const stateOverrides: StateOverride = [
|
|
1168
|
+
{
|
|
1169
|
+
address: this.rollupContract.address,
|
|
1170
|
+
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1171
|
+
stateDiff: [
|
|
1172
|
+
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1173
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1174
|
+
],
|
|
1175
|
+
},
|
|
1176
|
+
];
|
|
1177
|
+
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1178
|
+
if (this.proposerAddressForSimulation) {
|
|
1179
|
+
stateOverrides.push({
|
|
1180
|
+
address: this.proposerAddressForSimulation.toString(),
|
|
1181
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1000
1185
|
const simulationResult = await this.l1TxUtils
|
|
1001
1186
|
.simulate(
|
|
1002
1187
|
{
|
|
1003
1188
|
to: this.rollupContract.address,
|
|
1004
1189
|
data: rollupData,
|
|
1005
|
-
gas:
|
|
1190
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1191
|
+
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1006
1192
|
},
|
|
1007
1193
|
{
|
|
1008
1194
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1009
1195
|
time: timestamp + 1n,
|
|
1010
1196
|
// @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:
|
|
1197
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1012
1198
|
},
|
|
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
|
-
],
|
|
1199
|
+
stateOverrides,
|
|
1023
1200
|
RollupAbi,
|
|
1024
1201
|
{
|
|
1025
1202
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1026
|
-
fallbackGasEstimate:
|
|
1203
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1027
1204
|
},
|
|
1028
1205
|
)
|
|
1029
1206
|
.catch(err => {
|
|
1030
|
-
|
|
1207
|
+
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1208
|
+
const viemError = formatViemError(err);
|
|
1209
|
+
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1210
|
+
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1211
|
+
// Return a minimal simulation result with the fallback gas estimate
|
|
1212
|
+
return {
|
|
1213
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1214
|
+
logs: [],
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1031
1218
|
throw err;
|
|
1032
1219
|
});
|
|
1033
1220
|
|
|
@@ -1035,11 +1222,12 @@ export class SequencerPublisher {
|
|
|
1035
1222
|
}
|
|
1036
1223
|
|
|
1037
1224
|
private async addProposeTx(
|
|
1038
|
-
|
|
1225
|
+
checkpoint: Checkpoint,
|
|
1039
1226
|
encodedData: L1ProcessArgs,
|
|
1040
|
-
opts: { txTimeoutAt?: Date;
|
|
1227
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1041
1228
|
timestamp: bigint,
|
|
1042
1229
|
): Promise<void> {
|
|
1230
|
+
const slot = checkpoint.header.slotNumber;
|
|
1043
1231
|
const timer = new Timer();
|
|
1044
1232
|
const kzg = Blob.getViemKzgInstance();
|
|
1045
1233
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
@@ -1054,11 +1242,13 @@ export class SequencerPublisher {
|
|
|
1054
1242
|
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
1243
|
);
|
|
1056
1244
|
|
|
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
|
-
|
|
1245
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1246
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
1247
|
+
void Promise.resolve().then(() =>
|
|
1248
|
+
this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
|
|
1249
|
+
this.log.error('Failed to send blobs to blob client');
|
|
1250
|
+
}),
|
|
1251
|
+
);
|
|
1062
1252
|
|
|
1063
1253
|
return this.addRequest({
|
|
1064
1254
|
action: 'propose',
|
|
@@ -1066,7 +1256,7 @@ export class SequencerPublisher {
|
|
|
1066
1256
|
to: this.rollupContract.address,
|
|
1067
1257
|
data: rollupData,
|
|
1068
1258
|
},
|
|
1069
|
-
lastValidL2Slot:
|
|
1259
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1070
1260
|
gasConfig: { ...opts, gasLimit },
|
|
1071
1261
|
blobConfig: {
|
|
1072
1262
|
blobs: encodedData.blobs.map(b => b.data),
|
|
@@ -1080,12 +1270,13 @@ export class SequencerPublisher {
|
|
|
1080
1270
|
const success =
|
|
1081
1271
|
receipt &&
|
|
1082
1272
|
receipt.status === 'success' &&
|
|
1083
|
-
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
1273
|
+
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1274
|
+
|
|
1084
1275
|
if (success) {
|
|
1085
1276
|
const endBlock = receipt.blockNumber;
|
|
1086
1277
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1087
1278
|
const { calldataGas, calldataSize, sender } = stats!;
|
|
1088
|
-
const publishStats:
|
|
1279
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1089
1280
|
gasPrice: receipt.effectiveGasPrice,
|
|
1090
1281
|
gasUsed: receipt.gasUsed,
|
|
1091
1282
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
@@ -1094,23 +1285,26 @@ export class SequencerPublisher {
|
|
|
1094
1285
|
calldataGas,
|
|
1095
1286
|
calldataSize,
|
|
1096
1287
|
sender,
|
|
1097
|
-
...
|
|
1288
|
+
...checkpoint.getStats(),
|
|
1098
1289
|
eventName: 'rollup-published-to-l1',
|
|
1099
1290
|
blobCount: encodedData.blobs.length,
|
|
1100
1291
|
inclusionBlocks,
|
|
1101
1292
|
};
|
|
1102
|
-
this.log.info(`Published
|
|
1293
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
1294
|
+
...stats,
|
|
1295
|
+
...checkpoint.getStats(),
|
|
1296
|
+
...pick(receipt, 'transactionHash', 'blockHash'),
|
|
1297
|
+
});
|
|
1103
1298
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1104
1299
|
|
|
1105
1300
|
return true;
|
|
1106
1301
|
} else {
|
|
1107
1302
|
this.metrics.recordFailedTx('process');
|
|
1108
|
-
this.log.error(
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
});
|
|
1303
|
+
this.log.error(
|
|
1304
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1305
|
+
undefined,
|
|
1306
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1307
|
+
);
|
|
1114
1308
|
return false;
|
|
1115
1309
|
}
|
|
1116
1310
|
},
|