@aztec/sequencer-client 0.0.1-commit.fce3e4f → 0.0.1-commit.ffe5b04ea
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 +32 -16
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +118 -28
- package/dest/config.d.ts +33 -8
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +99 -44
- 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 +28 -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 +73 -47
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +888 -146
- package/dest/sequencer/checkpoint_proposal_job.d.ts +100 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job.js +1244 -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 +122 -133
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +717 -625
- package/dest/sequencer/timetable.d.ts +54 -16
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +147 -62
- package/dest/sequencer/types.d.ts +6 -0
- package/dest/sequencer/types.d.ts.map +1 -0
- package/dest/sequencer/types.js +1 -0
- package/dest/sequencer/utils.d.ts +14 -8
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +7 -4
- package/dest/test/index.d.ts +6 -7
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +95 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +231 -0
- package/dest/test/utils.d.ts +53 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +104 -0
- package/package.json +32 -30
- package/src/client/sequencer-client.ts +158 -52
- package/src/config.ts +114 -54
- 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 +43 -10
- package/src/publisher/sequencer-publisher-metrics.ts +19 -71
- package/src/publisher/sequencer-publisher.ts +587 -191
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/checkpoint_proposal_job.ts +960 -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 +178 -83
- package/src/sequencer/types.ts +9 -0
- package/src/sequencer/utils.ts +18 -9
- package/src/test/index.ts +5 -6
- package/src/test/mock_checkpoint_builder.ts +323 -0
- package/src/test/utils.ts +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,64 @@
|
|
|
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';
|
|
33
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
29
34
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
30
35
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
31
|
-
import type { Fr } from '@aztec/foundation/fields';
|
|
32
36
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
37
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
33
38
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
34
39
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
35
40
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
36
41
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
37
|
-
import {
|
|
42
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
43
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
38
44
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
39
45
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
40
|
-
import type {
|
|
41
|
-
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
42
|
-
|
|
43
|
-
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
46
|
+
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
47
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
44
48
|
|
|
45
|
-
import
|
|
49
|
+
import {
|
|
50
|
+
type Hex,
|
|
51
|
+
type StateOverride,
|
|
52
|
+
type TransactionReceipt,
|
|
53
|
+
type TypedDataDefinition,
|
|
54
|
+
encodeFunctionData,
|
|
55
|
+
keccak256,
|
|
56
|
+
multicall3Abi,
|
|
57
|
+
toHex,
|
|
58
|
+
} from 'viem';
|
|
59
|
+
|
|
60
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
61
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
46
62
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
47
63
|
|
|
48
64
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -57,6 +73,8 @@ type L1ProcessArgs = {
|
|
|
57
73
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
58
74
|
/** Attestations and signers signature */
|
|
59
75
|
attestationsAndSignersSignature: Signature;
|
|
76
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
77
|
+
feeAssetPriceModifier: bigint;
|
|
60
78
|
};
|
|
61
79
|
|
|
62
80
|
export const Actions = [
|
|
@@ -78,12 +96,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
|
|
|
78
96
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
79
97
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
80
98
|
|
|
81
|
-
export type
|
|
99
|
+
export type InvalidateCheckpointRequest = {
|
|
82
100
|
request: L1TxRequest;
|
|
83
101
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
84
102
|
gasUsed: bigint;
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
checkpointNumber: CheckpointNumber;
|
|
104
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
87
105
|
};
|
|
88
106
|
|
|
89
107
|
interface RequestWithExpiry {
|
|
@@ -102,23 +120,32 @@ export class SequencerPublisher {
|
|
|
102
120
|
private interrupted = false;
|
|
103
121
|
private metrics: SequencerPublisherMetrics;
|
|
104
122
|
public epochCache: EpochCache;
|
|
123
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
105
124
|
|
|
106
125
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
107
126
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
108
127
|
|
|
109
128
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
110
129
|
|
|
130
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
131
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
132
|
+
|
|
111
133
|
protected log: Logger;
|
|
112
134
|
protected ethereumSlotDuration: bigint;
|
|
113
135
|
|
|
114
|
-
private
|
|
136
|
+
private blobClient: BlobClientInterface;
|
|
115
137
|
|
|
116
138
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
117
139
|
private proposerAddressForSimulation?: EthAddress;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
|
|
141
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
142
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
143
|
+
|
|
144
|
+
/** L1 fee analyzer for fisherman mode */
|
|
145
|
+
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
146
|
+
|
|
147
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
148
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
122
149
|
|
|
123
150
|
// A CALL to a cold address is 2700 gas
|
|
124
151
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -126,20 +153,23 @@ export class SequencerPublisher {
|
|
|
126
153
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
127
154
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
128
155
|
|
|
129
|
-
public l1TxUtils:
|
|
156
|
+
public l1TxUtils: L1TxUtils;
|
|
130
157
|
public rollupContract: RollupContract;
|
|
131
158
|
public govProposerContract: GovernanceProposerContract;
|
|
132
159
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
133
160
|
public slashFactoryContract: SlashFactoryContract;
|
|
134
161
|
|
|
162
|
+
public readonly tracer: Tracer;
|
|
163
|
+
|
|
135
164
|
protected requests: RequestWithExpiry[] = [];
|
|
136
165
|
|
|
137
166
|
constructor(
|
|
138
|
-
private config:
|
|
167
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
168
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
139
169
|
deps: {
|
|
140
170
|
telemetry?: TelemetryClient;
|
|
141
|
-
|
|
142
|
-
l1TxUtils:
|
|
171
|
+
blobClient: BlobClientInterface;
|
|
172
|
+
l1TxUtils: L1TxUtils;
|
|
143
173
|
rollupContract: RollupContract;
|
|
144
174
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
145
175
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -149,6 +179,7 @@ export class SequencerPublisher {
|
|
|
149
179
|
metrics: SequencerPublisherMetrics;
|
|
150
180
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
151
181
|
log?: Logger;
|
|
182
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
152
183
|
},
|
|
153
184
|
) {
|
|
154
185
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -156,12 +187,13 @@ export class SequencerPublisher {
|
|
|
156
187
|
this.epochCache = deps.epochCache;
|
|
157
188
|
this.lastActions = deps.lastActions;
|
|
158
189
|
|
|
159
|
-
this.
|
|
160
|
-
deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
|
|
190
|
+
this.blobClient = deps.blobClient;
|
|
161
191
|
|
|
162
192
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
163
193
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
194
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
164
195
|
this.l1TxUtils = deps.l1TxUtils;
|
|
196
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
165
197
|
|
|
166
198
|
this.rollupContract = deps.rollupContract;
|
|
167
199
|
|
|
@@ -174,16 +206,72 @@ export class SequencerPublisher {
|
|
|
174
206
|
this.slashingProposerContract = newSlashingProposer;
|
|
175
207
|
});
|
|
176
208
|
this.slashFactoryContract = deps.slashFactoryContract;
|
|
209
|
+
|
|
210
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
211
|
+
if (config.fishermanMode) {
|
|
212
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
213
|
+
this.l1TxUtils.client,
|
|
214
|
+
deps.dateProvider,
|
|
215
|
+
createLogger('sequencer:publisher:fee-analyzer'),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Initialize fee asset price oracle
|
|
220
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
221
|
+
this.l1TxUtils.client,
|
|
222
|
+
this.rollupContract,
|
|
223
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
227
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
232
|
+
* Does nothing if no store is configured.
|
|
233
|
+
*/
|
|
234
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
235
|
+
if (!this.failedTxStore) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const tx: FailedL1Tx = {
|
|
240
|
+
...failedTx,
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Fire and forget - don't block on backup
|
|
245
|
+
void this.failedTxStore
|
|
246
|
+
.then(store => store?.saveFailedTx(tx))
|
|
247
|
+
.catch(err => {
|
|
248
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
249
|
+
});
|
|
177
250
|
}
|
|
178
251
|
|
|
179
252
|
public getRollupContract(): RollupContract {
|
|
180
253
|
return this.rollupContract;
|
|
181
254
|
}
|
|
182
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Gets the fee asset price modifier from the oracle.
|
|
258
|
+
* Returns 0n if the oracle query fails.
|
|
259
|
+
*/
|
|
260
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
261
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
262
|
+
}
|
|
263
|
+
|
|
183
264
|
public getSenderAddress() {
|
|
184
265
|
return this.l1TxUtils.getSenderAddress();
|
|
185
266
|
}
|
|
186
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
270
|
+
*/
|
|
271
|
+
public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
|
|
272
|
+
return this.l1FeeAnalyzer;
|
|
273
|
+
}
|
|
274
|
+
|
|
187
275
|
/**
|
|
188
276
|
* Sets the proposer address to use for simulations in fisherman mode.
|
|
189
277
|
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
@@ -211,6 +299,62 @@ export class SequencerPublisher {
|
|
|
211
299
|
}
|
|
212
300
|
}
|
|
213
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
304
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
305
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
306
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
307
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
308
|
+
*/
|
|
309
|
+
public async analyzeL1Fees(
|
|
310
|
+
l2SlotNumber: SlotNumber,
|
|
311
|
+
onComplete?: (analysis: L1FeeAnalysisResult) => void,
|
|
312
|
+
): Promise<L1FeeAnalysisResult | undefined> {
|
|
313
|
+
if (!this.l1FeeAnalyzer) {
|
|
314
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const requestsToAnalyze = [...this.requests];
|
|
319
|
+
if (requestsToAnalyze.length === 0) {
|
|
320
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Extract blob config from requests (if any)
|
|
325
|
+
const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
326
|
+
const blobConfig = blobConfigs[0];
|
|
327
|
+
|
|
328
|
+
// Get gas configs
|
|
329
|
+
const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
330
|
+
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
331
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
|
|
332
|
+
|
|
333
|
+
// Get the transaction requests
|
|
334
|
+
const l1Requests = requestsToAnalyze.map(r => r.request);
|
|
335
|
+
|
|
336
|
+
// Start the analysis
|
|
337
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
338
|
+
l2SlotNumber,
|
|
339
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
340
|
+
l1Requests,
|
|
341
|
+
blobConfig,
|
|
342
|
+
onComplete,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
this.log.info('Started L1 fee analysis', {
|
|
346
|
+
analysisId,
|
|
347
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
348
|
+
requestCount: requestsToAnalyze.length,
|
|
349
|
+
hasBlobConfig: !!blobConfig,
|
|
350
|
+
gasLimit: gasLimit.toString(),
|
|
351
|
+
actions: requestsToAnalyze.map(r => r.action),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
355
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
356
|
+
}
|
|
357
|
+
|
|
214
358
|
/**
|
|
215
359
|
* Sends all requests that are still valid.
|
|
216
360
|
* @returns one of:
|
|
@@ -218,10 +362,11 @@ export class SequencerPublisher {
|
|
|
218
362
|
* - a receipt and errorMsg if it failed on L1
|
|
219
363
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
220
364
|
*/
|
|
365
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
221
366
|
public async sendRequests() {
|
|
222
367
|
const requestsToProcess = [...this.requests];
|
|
223
368
|
this.requests = [];
|
|
224
|
-
if (this.interrupted) {
|
|
369
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
225
370
|
return undefined;
|
|
226
371
|
}
|
|
227
372
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -264,7 +409,16 @@ export class SequencerPublisher {
|
|
|
264
409
|
|
|
265
410
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
266
411
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
267
|
-
|
|
412
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
413
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
414
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
415
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
416
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
417
|
+
requested: gasLimit,
|
|
418
|
+
capped: maxGas,
|
|
419
|
+
});
|
|
420
|
+
gasLimit = maxGas;
|
|
421
|
+
}
|
|
268
422
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
269
423
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
270
424
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -274,19 +428,36 @@ export class SequencerPublisher {
|
|
|
274
428
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
275
429
|
|
|
276
430
|
try {
|
|
431
|
+
// Capture context for failed tx backup before sending
|
|
432
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
433
|
+
const multicallData = encodeFunctionData({
|
|
434
|
+
abi: multicall3Abi,
|
|
435
|
+
functionName: 'aggregate3',
|
|
436
|
+
args: [
|
|
437
|
+
validRequests.map(r => ({
|
|
438
|
+
target: r.request.to!,
|
|
439
|
+
callData: r.request.data!,
|
|
440
|
+
allowFailure: true,
|
|
441
|
+
})),
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
445
|
+
|
|
446
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
447
|
+
|
|
277
448
|
this.log.debug('Forwarding transactions', {
|
|
278
449
|
validRequests: validRequests.map(request => request.action),
|
|
279
450
|
txConfig,
|
|
280
451
|
});
|
|
281
|
-
const result = await
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
452
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
453
|
+
if (result === undefined) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
457
|
+
validRequests,
|
|
458
|
+
result,
|
|
459
|
+
txContext,
|
|
288
460
|
);
|
|
289
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
290
461
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
291
462
|
} catch (err) {
|
|
292
463
|
const viemError = formatViemError(err);
|
|
@@ -304,13 +475,76 @@ export class SequencerPublisher {
|
|
|
304
475
|
}
|
|
305
476
|
}
|
|
306
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
480
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
481
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
482
|
+
*/
|
|
483
|
+
private async forwardWithPublisherRotation(
|
|
484
|
+
validRequests: RequestWithExpiry[],
|
|
485
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
486
|
+
blobConfig: L1BlobInputs | undefined,
|
|
487
|
+
) {
|
|
488
|
+
const triedAddresses: EthAddress[] = [];
|
|
489
|
+
let currentPublisher = this.l1TxUtils;
|
|
490
|
+
|
|
491
|
+
while (true) {
|
|
492
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
493
|
+
try {
|
|
494
|
+
const result = await Multicall3.forward(
|
|
495
|
+
validRequests.map(r => r.request),
|
|
496
|
+
currentPublisher,
|
|
497
|
+
txConfig,
|
|
498
|
+
blobConfig,
|
|
499
|
+
this.rollupContract.address,
|
|
500
|
+
this.log,
|
|
501
|
+
);
|
|
502
|
+
this.l1TxUtils = currentPublisher;
|
|
503
|
+
return result;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
if (err instanceof TimeoutError) {
|
|
506
|
+
throw err;
|
|
507
|
+
}
|
|
508
|
+
const viemError = formatViemError(err);
|
|
509
|
+
if (!this.getNextPublisher) {
|
|
510
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
this.log.warn(
|
|
514
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
515
|
+
viemError,
|
|
516
|
+
);
|
|
517
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
518
|
+
if (!nextPublisher) {
|
|
519
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
currentPublisher = nextPublisher;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
307
527
|
private callbackBundledTransactions(
|
|
308
528
|
requests: RequestWithExpiry[],
|
|
309
|
-
result
|
|
529
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
530
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
310
531
|
) {
|
|
311
532
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
312
533
|
if (result instanceof FormattedViemError) {
|
|
313
534
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
535
|
+
this.backupFailedTx({
|
|
536
|
+
id: keccak256(txContext.multicallData),
|
|
537
|
+
failureType: 'send-error',
|
|
538
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
539
|
+
blobData: txContext.blobData,
|
|
540
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
541
|
+
error: { message: result.message, name: result.name },
|
|
542
|
+
context: {
|
|
543
|
+
actions: requests.map(r => r.action),
|
|
544
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
545
|
+
sender: this.getSenderAddress().toString(),
|
|
546
|
+
},
|
|
547
|
+
});
|
|
314
548
|
return { failedActions: requests.map(r => r.action) };
|
|
315
549
|
} else {
|
|
316
550
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -323,6 +557,30 @@ export class SequencerPublisher {
|
|
|
323
557
|
failedActions.push(request.action);
|
|
324
558
|
}
|
|
325
559
|
}
|
|
560
|
+
// Single backup for the whole reverted tx
|
|
561
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
562
|
+
this.backupFailedTx({
|
|
563
|
+
id: result.receipt.transactionHash,
|
|
564
|
+
failureType: 'revert',
|
|
565
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
566
|
+
blobData: txContext.blobData,
|
|
567
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
568
|
+
receipt: {
|
|
569
|
+
transactionHash: result.receipt.transactionHash,
|
|
570
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
571
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
572
|
+
status: 'reverted',
|
|
573
|
+
},
|
|
574
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
575
|
+
context: {
|
|
576
|
+
actions: failedActions,
|
|
577
|
+
requests: requests
|
|
578
|
+
.filter(r => failedActions.includes(r.action))
|
|
579
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
580
|
+
sender: this.getSenderAddress().toString(),
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
}
|
|
326
584
|
return { successfulActions, failedActions };
|
|
327
585
|
}
|
|
328
586
|
}
|
|
@@ -335,14 +593,14 @@ export class SequencerPublisher {
|
|
|
335
593
|
public canProposeAtNextEthBlock(
|
|
336
594
|
tipArchive: Fr,
|
|
337
595
|
msgSender: EthAddress,
|
|
338
|
-
opts: {
|
|
596
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
339
597
|
) {
|
|
340
598
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
341
599
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
342
600
|
|
|
343
601
|
return this.rollupContract
|
|
344
602
|
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
345
|
-
forcePendingCheckpointNumber: opts.
|
|
603
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
346
604
|
})
|
|
347
605
|
.catch(err => {
|
|
348
606
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
@@ -361,7 +619,11 @@ export class SequencerPublisher {
|
|
|
361
619
|
* It will throw if the block header is invalid.
|
|
362
620
|
* @param header - The block header to validate
|
|
363
621
|
*/
|
|
364
|
-
|
|
622
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
623
|
+
public async validateBlockHeader(
|
|
624
|
+
header: CheckpointHeader,
|
|
625
|
+
opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
|
|
626
|
+
): Promise<void> {
|
|
365
627
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
366
628
|
|
|
367
629
|
const args = [
|
|
@@ -370,12 +632,14 @@ export class SequencerPublisher {
|
|
|
370
632
|
[], // no signers
|
|
371
633
|
Signature.empty().toViemSignature(),
|
|
372
634
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
373
|
-
header.
|
|
635
|
+
header.blobsHash.toString(),
|
|
374
636
|
flags,
|
|
375
637
|
] as const;
|
|
376
638
|
|
|
377
639
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
378
|
-
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
640
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
641
|
+
opts?.forcePendingCheckpointNumber,
|
|
642
|
+
);
|
|
379
643
|
let balance = 0n;
|
|
380
644
|
if (this.config.fishermanMode) {
|
|
381
645
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -402,77 +666,109 @@ export class SequencerPublisher {
|
|
|
402
666
|
}
|
|
403
667
|
|
|
404
668
|
/**
|
|
405
|
-
* Simulate making a call to invalidate a
|
|
406
|
-
* @param
|
|
669
|
+
* Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
|
|
670
|
+
* @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
|
|
407
671
|
*/
|
|
408
|
-
public async
|
|
409
|
-
validationResult:
|
|
410
|
-
): Promise<
|
|
672
|
+
public async simulateInvalidateCheckpoint(
|
|
673
|
+
validationResult: ValidateCheckpointResult,
|
|
674
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
411
675
|
if (validationResult.valid) {
|
|
412
676
|
return undefined;
|
|
413
677
|
}
|
|
414
678
|
|
|
415
|
-
const { reason,
|
|
416
|
-
const
|
|
417
|
-
const logData = { ...
|
|
679
|
+
const { reason, checkpoint } = validationResult;
|
|
680
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
681
|
+
const logData = { ...checkpoint, reason };
|
|
418
682
|
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
683
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
684
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
421
685
|
this.log.verbose(
|
|
422
|
-
`Skipping
|
|
423
|
-
{
|
|
686
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
687
|
+
{ currentCheckpointNumber, ...logData },
|
|
424
688
|
);
|
|
425
689
|
return undefined;
|
|
426
690
|
}
|
|
427
691
|
|
|
428
|
-
const request = this.
|
|
429
|
-
this.log.debug(`Simulating invalidate
|
|
692
|
+
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
693
|
+
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
694
|
+
|
|
695
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
430
696
|
|
|
431
697
|
try {
|
|
432
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
433
|
-
|
|
698
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
699
|
+
request,
|
|
700
|
+
undefined,
|
|
701
|
+
undefined,
|
|
702
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
703
|
+
);
|
|
704
|
+
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
705
|
+
...logData,
|
|
706
|
+
request,
|
|
707
|
+
gasUsed,
|
|
708
|
+
});
|
|
434
709
|
|
|
435
|
-
return {
|
|
710
|
+
return {
|
|
711
|
+
request,
|
|
712
|
+
gasUsed,
|
|
713
|
+
checkpointNumber,
|
|
714
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
715
|
+
reason,
|
|
716
|
+
};
|
|
436
717
|
} catch (err) {
|
|
437
718
|
const viemError = formatViemError(err);
|
|
438
719
|
|
|
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('
|
|
720
|
+
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
721
|
+
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
722
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
442
723
|
this.log.verbose(
|
|
443
|
-
`Simulation for invalidate
|
|
724
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
444
725
|
{ ...logData, request, error: viemError.message },
|
|
445
726
|
);
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
this.log.verbose(`
|
|
727
|
+
const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
728
|
+
if (latestPendingCheckpointNumber < checkpointNumber) {
|
|
729
|
+
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
449
730
|
return undefined;
|
|
450
731
|
} else {
|
|
451
732
|
this.log.error(
|
|
452
|
-
`Simulation for invalidate ${
|
|
733
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
453
734
|
viemError,
|
|
454
735
|
logData,
|
|
455
736
|
);
|
|
456
|
-
throw new Error(
|
|
457
|
-
|
|
458
|
-
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
739
|
+
{
|
|
740
|
+
cause: viemError,
|
|
741
|
+
},
|
|
742
|
+
);
|
|
459
743
|
}
|
|
460
744
|
}
|
|
461
745
|
|
|
462
|
-
// Otherwise, throw. We cannot build the next
|
|
463
|
-
this.log.error(`Simulation for invalidate
|
|
464
|
-
|
|
746
|
+
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
747
|
+
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
748
|
+
this.backupFailedTx({
|
|
749
|
+
id: keccak256(request.data!),
|
|
750
|
+
failureType: 'simulation',
|
|
751
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
752
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
753
|
+
error: { message: viemError.message, name: viemError.name },
|
|
754
|
+
context: {
|
|
755
|
+
actions: [`invalidate-${reason}`],
|
|
756
|
+
checkpointNumber,
|
|
757
|
+
sender: this.getSenderAddress().toString(),
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
465
761
|
}
|
|
466
762
|
}
|
|
467
763
|
|
|
468
|
-
private
|
|
764
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
469
765
|
if (validationResult.valid) {
|
|
470
|
-
throw new Error('Cannot invalidate a valid
|
|
766
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
471
767
|
}
|
|
472
768
|
|
|
473
|
-
const {
|
|
474
|
-
const logData = { ...
|
|
475
|
-
this.log.debug(`
|
|
769
|
+
const { checkpoint, committee, reason } = validationResult;
|
|
770
|
+
const logData = { ...checkpoint, reason };
|
|
771
|
+
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
476
772
|
|
|
477
773
|
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
478
774
|
validationResult.attestations,
|
|
@@ -480,14 +776,14 @@ export class SequencerPublisher {
|
|
|
480
776
|
|
|
481
777
|
if (reason === 'invalid-attestation') {
|
|
482
778
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
483
|
-
|
|
779
|
+
checkpoint.checkpointNumber,
|
|
484
780
|
attestationsAndSigners,
|
|
485
781
|
committee,
|
|
486
782
|
validationResult.invalidIndex,
|
|
487
783
|
);
|
|
488
784
|
} else if (reason === 'insufficient-attestations') {
|
|
489
785
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
490
|
-
|
|
786
|
+
checkpoint.checkpointNumber,
|
|
491
787
|
attestationsAndSigners,
|
|
492
788
|
committee,
|
|
493
789
|
);
|
|
@@ -497,47 +793,25 @@ export class SequencerPublisher {
|
|
|
497
793
|
}
|
|
498
794
|
}
|
|
499
795
|
|
|
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,
|
|
796
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
797
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
798
|
+
public async validateCheckpointForSubmission(
|
|
799
|
+
checkpoint: Checkpoint,
|
|
511
800
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
512
801
|
attestationsAndSignersSignature: Signature,
|
|
513
|
-
options: {
|
|
802
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
514
803
|
): Promise<bigint> {
|
|
515
804
|
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);
|
|
805
|
+
const blobFields = checkpoint.toBlobFields();
|
|
806
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
533
807
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
534
808
|
|
|
535
809
|
const args = [
|
|
536
810
|
{
|
|
537
|
-
header:
|
|
538
|
-
archive: toHex(
|
|
811
|
+
header: checkpoint.header.toViem(),
|
|
812
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
539
813
|
oracleInput: {
|
|
540
|
-
feeAssetPriceModifier:
|
|
814
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
541
815
|
},
|
|
542
816
|
},
|
|
543
817
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -573,10 +847,45 @@ export class SequencerPublisher {
|
|
|
573
847
|
const round = await base.computeRound(slotNumber);
|
|
574
848
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
575
849
|
|
|
850
|
+
if (roundInfo.quorumReached) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
|
|
576
854
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
577
855
|
return false;
|
|
578
856
|
}
|
|
579
857
|
|
|
858
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
859
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Check if payload was already submitted to governance
|
|
864
|
+
const cacheKey = payload.toString();
|
|
865
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
866
|
+
try {
|
|
867
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
868
|
+
const proposed = await retry(
|
|
869
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
870
|
+
'Check if payload was proposed',
|
|
871
|
+
makeBackoff([0, 1, 2]),
|
|
872
|
+
this.log,
|
|
873
|
+
true,
|
|
874
|
+
);
|
|
875
|
+
if (proposed) {
|
|
876
|
+
this.payloadProposedCache.add(cacheKey);
|
|
877
|
+
}
|
|
878
|
+
} catch (err) {
|
|
879
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
885
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
|
|
580
889
|
const cachedLastVote = this.lastActions[signalType];
|
|
581
890
|
this.lastActions[signalType] = slotNumber;
|
|
582
891
|
const action = signalType;
|
|
@@ -595,11 +904,26 @@ export class SequencerPublisher {
|
|
|
595
904
|
lastValidL2Slot: slotNumber,
|
|
596
905
|
});
|
|
597
906
|
|
|
907
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
908
|
+
|
|
598
909
|
try {
|
|
599
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
910
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
600
911
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
601
912
|
} catch (err) {
|
|
602
|
-
|
|
913
|
+
const viemError = formatViemError(err);
|
|
914
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
915
|
+
this.backupFailedTx({
|
|
916
|
+
id: keccak256(request.data!),
|
|
917
|
+
failureType: 'simulation',
|
|
918
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
919
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
920
|
+
error: { message: viemError.message, name: viemError.name },
|
|
921
|
+
context: {
|
|
922
|
+
actions: [action],
|
|
923
|
+
slot: slotNumber,
|
|
924
|
+
sender: this.getSenderAddress().toString(),
|
|
925
|
+
},
|
|
926
|
+
});
|
|
603
927
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
604
928
|
}
|
|
605
929
|
|
|
@@ -619,14 +943,14 @@ export class SequencerPublisher {
|
|
|
619
943
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
620
944
|
if (!success) {
|
|
621
945
|
this.log.error(
|
|
622
|
-
`Signaling in
|
|
946
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
623
947
|
logData,
|
|
624
948
|
);
|
|
625
949
|
this.lastActions[signalType] = cachedLastVote;
|
|
626
950
|
return false;
|
|
627
951
|
} else {
|
|
628
952
|
this.log.info(
|
|
629
|
-
`Signaling in
|
|
953
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
630
954
|
logData,
|
|
631
955
|
);
|
|
632
956
|
return true;
|
|
@@ -636,6 +960,17 @@ export class SequencerPublisher {
|
|
|
636
960
|
return true;
|
|
637
961
|
}
|
|
638
962
|
|
|
963
|
+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
964
|
+
const key = payload.toString();
|
|
965
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
966
|
+
if (cached) {
|
|
967
|
+
return cached;
|
|
968
|
+
}
|
|
969
|
+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
970
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
971
|
+
return isEmpty;
|
|
972
|
+
}
|
|
973
|
+
|
|
639
974
|
/**
|
|
640
975
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
641
976
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -783,30 +1118,25 @@ export class SequencerPublisher {
|
|
|
783
1118
|
return true;
|
|
784
1119
|
}
|
|
785
1120
|
|
|
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,
|
|
1121
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
1122
|
+
public async enqueueProposeCheckpoint(
|
|
1123
|
+
checkpoint: Checkpoint,
|
|
794
1124
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
795
1125
|
attestationsAndSignersSignature: Signature,
|
|
796
|
-
opts: { txTimeoutAt?: Date;
|
|
797
|
-
): Promise<
|
|
798
|
-
const checkpointHeader =
|
|
1126
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1127
|
+
): Promise<void> {
|
|
1128
|
+
const checkpointHeader = checkpoint.header;
|
|
799
1129
|
|
|
800
|
-
const blobFields =
|
|
801
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1130
|
+
const blobFields = checkpoint.toBlobFields();
|
|
1131
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
802
1132
|
|
|
803
|
-
const proposeTxArgs = {
|
|
1133
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
804
1134
|
header: checkpointHeader,
|
|
805
|
-
archive:
|
|
806
|
-
body: block.body.toBuffer(),
|
|
1135
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
807
1136
|
blobs,
|
|
808
1137
|
attestationsAndSigners,
|
|
809
1138
|
attestationsAndSignersSignature,
|
|
1139
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
810
1140
|
};
|
|
811
1141
|
|
|
812
1142
|
let ts: bigint;
|
|
@@ -817,22 +1147,29 @@ export class SequencerPublisher {
|
|
|
817
1147
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
818
1148
|
// make time consistency checks break.
|
|
819
1149
|
// 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.
|
|
1150
|
+
ts = await this.validateCheckpointForSubmission(
|
|
1151
|
+
checkpoint,
|
|
1152
|
+
attestationsAndSigners,
|
|
1153
|
+
attestationsAndSignersSignature,
|
|
1154
|
+
opts,
|
|
1155
|
+
);
|
|
821
1156
|
} catch (err: any) {
|
|
822
|
-
this.log.error(`
|
|
823
|
-
...
|
|
824
|
-
slotNumber:
|
|
825
|
-
|
|
1157
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1158
|
+
...checkpoint.getStats(),
|
|
1159
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
1160
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
826
1161
|
});
|
|
827
1162
|
throw err;
|
|
828
1163
|
}
|
|
829
1164
|
|
|
830
|
-
this.log.verbose(`Enqueuing
|
|
831
|
-
await this.addProposeTx(
|
|
832
|
-
return true;
|
|
1165
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1166
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
833
1167
|
}
|
|
834
1168
|
|
|
835
|
-
public
|
|
1169
|
+
public enqueueInvalidateCheckpoint(
|
|
1170
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
1171
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
1172
|
+
) {
|
|
836
1173
|
if (!request) {
|
|
837
1174
|
return;
|
|
838
1175
|
}
|
|
@@ -840,9 +1177,9 @@ export class SequencerPublisher {
|
|
|
840
1177
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
841
1178
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
842
1179
|
|
|
843
|
-
const { gasUsed,
|
|
844
|
-
const logData = { gasUsed,
|
|
845
|
-
this.log.verbose(`Enqueuing invalidate
|
|
1180
|
+
const { gasUsed, checkpointNumber } = request;
|
|
1181
|
+
const logData = { gasUsed, checkpointNumber, gasLimit, opts };
|
|
1182
|
+
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
846
1183
|
this.addRequest({
|
|
847
1184
|
action: `invalidate-by-${request.reason}`,
|
|
848
1185
|
request: request.request,
|
|
@@ -855,9 +1192,9 @@ export class SequencerPublisher {
|
|
|
855
1192
|
result.receipt.status === 'success' &&
|
|
856
1193
|
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
857
1194
|
if (!success) {
|
|
858
|
-
this.log.warn(`Invalidate
|
|
1195
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
859
1196
|
} else {
|
|
860
|
-
this.log.info(`Invalidate
|
|
1197
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
861
1198
|
}
|
|
862
1199
|
return !!success;
|
|
863
1200
|
},
|
|
@@ -882,13 +1219,30 @@ export class SequencerPublisher {
|
|
|
882
1219
|
|
|
883
1220
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
884
1221
|
|
|
1222
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1223
|
+
|
|
885
1224
|
let gasUsed: bigint;
|
|
1225
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
886
1226
|
try {
|
|
887
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1227
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
888
1228
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
889
1229
|
} catch (err) {
|
|
890
|
-
const viemError = formatViemError(err);
|
|
1230
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
891
1231
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1232
|
+
|
|
1233
|
+
this.backupFailedTx({
|
|
1234
|
+
id: keccak256(request.data!),
|
|
1235
|
+
failureType: 'simulation',
|
|
1236
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1237
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1238
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1239
|
+
context: {
|
|
1240
|
+
actions: [action],
|
|
1241
|
+
slot: slotNumber,
|
|
1242
|
+
sender: this.getSenderAddress().toString(),
|
|
1243
|
+
},
|
|
1244
|
+
});
|
|
1245
|
+
|
|
892
1246
|
return false;
|
|
893
1247
|
}
|
|
894
1248
|
|
|
@@ -896,10 +1250,14 @@ export class SequencerPublisher {
|
|
|
896
1250
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
897
1251
|
logData.gasLimit = gasLimit;
|
|
898
1252
|
|
|
1253
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1254
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1255
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1256
|
+
|
|
899
1257
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
900
1258
|
this.addRequest({
|
|
901
1259
|
action,
|
|
902
|
-
request,
|
|
1260
|
+
request: requestWithAbi,
|
|
903
1261
|
gasConfig: { gasLimit },
|
|
904
1262
|
lastValidL2Slot: slotNumber,
|
|
905
1263
|
checkSuccess: (_req, result) => {
|
|
@@ -936,7 +1294,7 @@ export class SequencerPublisher {
|
|
|
936
1294
|
private async prepareProposeTx(
|
|
937
1295
|
encodedData: L1ProcessArgs,
|
|
938
1296
|
timestamp: bigint,
|
|
939
|
-
options: {
|
|
1297
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
940
1298
|
) {
|
|
941
1299
|
const kzg = Blob.getViemKzgInstance();
|
|
942
1300
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
@@ -968,9 +1326,27 @@ export class SequencerPublisher {
|
|
|
968
1326
|
kzg,
|
|
969
1327
|
},
|
|
970
1328
|
)
|
|
971
|
-
.catch(err => {
|
|
972
|
-
const
|
|
973
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1329
|
+
.catch(async err => {
|
|
1330
|
+
const viemError = formatViemError(err);
|
|
1331
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1332
|
+
const validateBlobsData = encodeFunctionData({
|
|
1333
|
+
abi: RollupAbi,
|
|
1334
|
+
functionName: 'validateBlobs',
|
|
1335
|
+
args: [blobInput],
|
|
1336
|
+
});
|
|
1337
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1338
|
+
this.backupFailedTx({
|
|
1339
|
+
id: keccak256(validateBlobsData),
|
|
1340
|
+
failureType: 'simulation',
|
|
1341
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1342
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1343
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1344
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1345
|
+
context: {
|
|
1346
|
+
actions: ['validate-blobs'],
|
|
1347
|
+
sender: this.getSenderAddress().toString(),
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
974
1350
|
throw new Error('Failed to validate blobs');
|
|
975
1351
|
});
|
|
976
1352
|
}
|
|
@@ -981,8 +1357,7 @@ export class SequencerPublisher {
|
|
|
981
1357
|
header: encodedData.header.toViem(),
|
|
982
1358
|
archive: toHex(encodedData.archive),
|
|
983
1359
|
oracleInput: {
|
|
984
|
-
|
|
985
|
-
feeAssetPriceModifier: 0n,
|
|
1360
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
986
1361
|
},
|
|
987
1362
|
},
|
|
988
1363
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1008,7 +1383,7 @@ export class SequencerPublisher {
|
|
|
1008
1383
|
readonly header: ViemHeader;
|
|
1009
1384
|
readonly archive: `0x${string}`;
|
|
1010
1385
|
readonly oracleInput: {
|
|
1011
|
-
readonly feeAssetPriceModifier:
|
|
1386
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1012
1387
|
};
|
|
1013
1388
|
},
|
|
1014
1389
|
ViemCommitteeAttestations,
|
|
@@ -1017,7 +1392,7 @@ export class SequencerPublisher {
|
|
|
1017
1392
|
`0x${string}`,
|
|
1018
1393
|
],
|
|
1019
1394
|
timestamp: bigint,
|
|
1020
|
-
options: {
|
|
1395
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1021
1396
|
) {
|
|
1022
1397
|
const rollupData = encodeFunctionData({
|
|
1023
1398
|
abi: RollupAbi,
|
|
@@ -1025,10 +1400,10 @@ export class SequencerPublisher {
|
|
|
1025
1400
|
args,
|
|
1026
1401
|
});
|
|
1027
1402
|
|
|
1028
|
-
// override the pending
|
|
1029
|
-
const
|
|
1030
|
-
options.
|
|
1031
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.
|
|
1403
|
+
// override the pending checkpoint number if requested
|
|
1404
|
+
const forcePendingCheckpointNumberStateDiff = (
|
|
1405
|
+
options.forcePendingCheckpointNumber !== undefined
|
|
1406
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1032
1407
|
: []
|
|
1033
1408
|
).flatMap(override => override.stateDiff ?? []);
|
|
1034
1409
|
|
|
@@ -1038,7 +1413,7 @@ export class SequencerPublisher {
|
|
|
1038
1413
|
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1039
1414
|
stateDiff: [
|
|
1040
1415
|
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1041
|
-
...
|
|
1416
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1042
1417
|
],
|
|
1043
1418
|
},
|
|
1044
1419
|
];
|
|
@@ -1050,25 +1425,27 @@ export class SequencerPublisher {
|
|
|
1050
1425
|
});
|
|
1051
1426
|
}
|
|
1052
1427
|
|
|
1428
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1429
|
+
|
|
1053
1430
|
const simulationResult = await this.l1TxUtils
|
|
1054
1431
|
.simulate(
|
|
1055
1432
|
{
|
|
1056
1433
|
to: this.rollupContract.address,
|
|
1057
1434
|
data: rollupData,
|
|
1058
|
-
gas:
|
|
1435
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1059
1436
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1060
1437
|
},
|
|
1061
1438
|
{
|
|
1062
1439
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1063
1440
|
time: timestamp + 1n,
|
|
1064
1441
|
// @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:
|
|
1442
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1066
1443
|
},
|
|
1067
1444
|
stateOverrides,
|
|
1068
1445
|
RollupAbi,
|
|
1069
1446
|
{
|
|
1070
1447
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1071
|
-
fallbackGasEstimate:
|
|
1448
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1072
1449
|
},
|
|
1073
1450
|
)
|
|
1074
1451
|
.catch(err => {
|
|
@@ -1078,11 +1455,23 @@ export class SequencerPublisher {
|
|
|
1078
1455
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1079
1456
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1080
1457
|
return {
|
|
1081
|
-
gasUsed:
|
|
1458
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1082
1459
|
logs: [],
|
|
1083
1460
|
};
|
|
1084
1461
|
}
|
|
1085
1462
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1463
|
+
this.backupFailedTx({
|
|
1464
|
+
id: keccak256(rollupData),
|
|
1465
|
+
failureType: 'simulation',
|
|
1466
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1467
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1468
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1469
|
+
context: {
|
|
1470
|
+
actions: ['propose'],
|
|
1471
|
+
slot: Number(args[0].header.slotNumber),
|
|
1472
|
+
sender: this.getSenderAddress().toString(),
|
|
1473
|
+
},
|
|
1474
|
+
});
|
|
1086
1475
|
throw err;
|
|
1087
1476
|
});
|
|
1088
1477
|
|
|
@@ -1090,11 +1479,12 @@ export class SequencerPublisher {
|
|
|
1090
1479
|
}
|
|
1091
1480
|
|
|
1092
1481
|
private async addProposeTx(
|
|
1093
|
-
|
|
1482
|
+
checkpoint: Checkpoint,
|
|
1094
1483
|
encodedData: L1ProcessArgs,
|
|
1095
|
-
opts: { txTimeoutAt?: Date;
|
|
1484
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1096
1485
|
timestamp: bigint,
|
|
1097
1486
|
): Promise<void> {
|
|
1487
|
+
const slot = checkpoint.header.slotNumber;
|
|
1098
1488
|
const timer = new Timer();
|
|
1099
1489
|
const kzg = Blob.getViemKzgInstance();
|
|
1100
1490
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
@@ -1109,11 +1499,13 @@ export class SequencerPublisher {
|
|
|
1109
1499
|
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
1500
|
);
|
|
1111
1501
|
|
|
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
|
-
|
|
1502
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1503
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
1504
|
+
void Promise.resolve().then(() =>
|
|
1505
|
+
this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
|
|
1506
|
+
this.log.error('Failed to send blobs to blob client');
|
|
1507
|
+
}),
|
|
1508
|
+
);
|
|
1117
1509
|
|
|
1118
1510
|
return this.addRequest({
|
|
1119
1511
|
action: 'propose',
|
|
@@ -1121,7 +1513,7 @@ export class SequencerPublisher {
|
|
|
1121
1513
|
to: this.rollupContract.address,
|
|
1122
1514
|
data: rollupData,
|
|
1123
1515
|
},
|
|
1124
|
-
lastValidL2Slot:
|
|
1516
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1125
1517
|
gasConfig: { ...opts, gasLimit },
|
|
1126
1518
|
blobConfig: {
|
|
1127
1519
|
blobs: encodedData.blobs.map(b => b.data),
|
|
@@ -1136,11 +1528,12 @@ export class SequencerPublisher {
|
|
|
1136
1528
|
receipt &&
|
|
1137
1529
|
receipt.status === 'success' &&
|
|
1138
1530
|
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1531
|
+
|
|
1139
1532
|
if (success) {
|
|
1140
1533
|
const endBlock = receipt.blockNumber;
|
|
1141
1534
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1142
1535
|
const { calldataGas, calldataSize, sender } = stats!;
|
|
1143
|
-
const publishStats:
|
|
1536
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1144
1537
|
gasPrice: receipt.effectiveGasPrice,
|
|
1145
1538
|
gasUsed: receipt.gasUsed,
|
|
1146
1539
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
@@ -1149,23 +1542,26 @@ export class SequencerPublisher {
|
|
|
1149
1542
|
calldataGas,
|
|
1150
1543
|
calldataSize,
|
|
1151
1544
|
sender,
|
|
1152
|
-
...
|
|
1545
|
+
...checkpoint.getStats(),
|
|
1153
1546
|
eventName: 'rollup-published-to-l1',
|
|
1154
1547
|
blobCount: encodedData.blobs.length,
|
|
1155
1548
|
inclusionBlocks,
|
|
1156
1549
|
};
|
|
1157
|
-
this.log.info(`Published
|
|
1550
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
1551
|
+
...stats,
|
|
1552
|
+
...checkpoint.getStats(),
|
|
1553
|
+
...pick(receipt, 'transactionHash', 'blockHash'),
|
|
1554
|
+
});
|
|
1158
1555
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1159
1556
|
|
|
1160
1557
|
return true;
|
|
1161
1558
|
} else {
|
|
1162
1559
|
this.metrics.recordFailedTx('process');
|
|
1163
|
-
this.log.error(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
});
|
|
1560
|
+
this.log.error(
|
|
1561
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1562
|
+
undefined,
|
|
1563
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1564
|
+
);
|
|
1169
1565
|
return false;
|
|
1170
1566
|
}
|
|
1171
1567
|
},
|