@aztec/sequencer-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.04852196a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +26 -11
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +99 -16
- package/dest/config.d.ts +24 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +45 -28
- package/dest/global_variable_builder/global_builder.d.ts +5 -7
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +13 -13
- 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 +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -42
- 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 +12 -4
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- 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 +44 -25
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +781 -101
- package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -13
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +683 -79
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/index.d.ts +1 -3
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -2
- package/dest/sequencer/metrics.d.ts +19 -7
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +131 -141
- package/dest/sequencer/sequencer.d.ts +46 -23
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +514 -67
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +5 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +4 -7
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +28 -16
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +86 -34
- package/dest/test/utils.d.ts +13 -9
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +27 -17
- package/package.json +30 -28
- package/src/client/sequencer-client.ts +139 -23
- package/src/config.ts +59 -38
- package/src/global_variable_builder/global_builder.ts +14 -14
- package/src/index.ts +1 -9
- package/src/publisher/config.ts +121 -43
- 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 +39 -7
- package/src/publisher/sequencer-publisher-metrics.ts +17 -69
- package/src/publisher/sequencer-publisher.ts +420 -137
- package/src/sequencer/checkpoint_proposal_job.ts +361 -104
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/metrics.ts +132 -148
- package/src/sequencer/sequencer.ts +160 -69
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +4 -1
- package/src/test/index.ts +3 -6
- package/src/test/mock_checkpoint_builder.ts +147 -71
- package/src/test/utils.ts +58 -28
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/dest/sequencer/checkpoint_builder.d.ts +0 -63
- package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
- package/dest/sequencer/checkpoint_builder.js +0 -131
- 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 -18
- 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 -217
- package/src/sequencer/checkpoint_builder.ts +0 -217
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -133
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
+
FeeAssetPriceOracle,
|
|
7
8
|
type GovernanceProposerContract,
|
|
8
9
|
type IEmpireBase,
|
|
9
10
|
MULTI_CALL_3_ADDRESS,
|
|
@@ -18,33 +19,46 @@ import {
|
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
23
|
+
MAX_L1_TX_LIMIT,
|
|
21
24
|
type TransactionStats,
|
|
22
25
|
WEI_CONST,
|
|
23
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
24
|
-
import
|
|
25
|
-
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/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 {
|
|
30
|
+
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
29
31
|
import { pick } from '@aztec/foundation/collection';
|
|
30
32
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
33
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
31
34
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
32
35
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
33
36
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
37
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
34
38
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
35
39
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
36
40
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
37
41
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
38
|
-
import { CommitteeAttestationsAndSigners, type
|
|
42
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
39
43
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
40
44
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
41
45
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
42
46
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
43
|
-
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
44
48
|
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
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';
|
|
48
62
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
49
63
|
|
|
50
64
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -59,6 +73,8 @@ type L1ProcessArgs = {
|
|
|
59
73
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
60
74
|
/** Attestations and signers signature */
|
|
61
75
|
attestationsAndSignersSignature: Signature;
|
|
76
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
77
|
+
feeAssetPriceModifier: bigint;
|
|
62
78
|
};
|
|
63
79
|
|
|
64
80
|
export const Actions = [
|
|
@@ -80,12 +96,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
|
|
|
80
96
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
81
97
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
82
98
|
|
|
83
|
-
export type
|
|
99
|
+
export type InvalidateCheckpointRequest = {
|
|
84
100
|
request: L1TxRequest;
|
|
85
101
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
86
102
|
gasUsed: bigint;
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
checkpointNumber: CheckpointNumber;
|
|
104
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
89
105
|
};
|
|
90
106
|
|
|
91
107
|
interface RequestWithExpiry {
|
|
@@ -104,6 +120,7 @@ export class SequencerPublisher {
|
|
|
104
120
|
private interrupted = false;
|
|
105
121
|
private metrics: SequencerPublisherMetrics;
|
|
106
122
|
public epochCache: EpochCache;
|
|
123
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
107
124
|
|
|
108
125
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
109
126
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -111,6 +128,7 @@ export class SequencerPublisher {
|
|
|
111
128
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
112
129
|
|
|
113
130
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
131
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
114
132
|
|
|
115
133
|
protected log: Logger;
|
|
116
134
|
protected ethereumSlotDuration: bigint;
|
|
@@ -120,12 +138,14 @@ export class SequencerPublisher {
|
|
|
120
138
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
121
139
|
private proposerAddressForSimulation?: EthAddress;
|
|
122
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
|
+
|
|
123
144
|
/** L1 fee analyzer for fisherman mode */
|
|
124
145
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
|
|
146
|
+
|
|
147
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
148
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
129
149
|
|
|
130
150
|
// A CALL to a cold address is 2700 gas
|
|
131
151
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -133,20 +153,23 @@ export class SequencerPublisher {
|
|
|
133
153
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
134
154
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
135
155
|
|
|
136
|
-
public l1TxUtils:
|
|
156
|
+
public l1TxUtils: L1TxUtils;
|
|
137
157
|
public rollupContract: RollupContract;
|
|
138
158
|
public govProposerContract: GovernanceProposerContract;
|
|
139
159
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
140
160
|
public slashFactoryContract: SlashFactoryContract;
|
|
141
161
|
|
|
162
|
+
public readonly tracer: Tracer;
|
|
163
|
+
|
|
142
164
|
protected requests: RequestWithExpiry[] = [];
|
|
143
165
|
|
|
144
166
|
constructor(
|
|
145
|
-
private config:
|
|
167
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
168
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
146
169
|
deps: {
|
|
147
170
|
telemetry?: TelemetryClient;
|
|
148
|
-
blobClient
|
|
149
|
-
l1TxUtils:
|
|
171
|
+
blobClient: BlobClientInterface;
|
|
172
|
+
l1TxUtils: L1TxUtils;
|
|
150
173
|
rollupContract: RollupContract;
|
|
151
174
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
152
175
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -156,6 +179,7 @@ export class SequencerPublisher {
|
|
|
156
179
|
metrics: SequencerPublisherMetrics;
|
|
157
180
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
158
181
|
log?: Logger;
|
|
182
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
159
183
|
},
|
|
160
184
|
) {
|
|
161
185
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -163,12 +187,13 @@ export class SequencerPublisher {
|
|
|
163
187
|
this.epochCache = deps.epochCache;
|
|
164
188
|
this.lastActions = deps.lastActions;
|
|
165
189
|
|
|
166
|
-
this.blobClient =
|
|
167
|
-
deps.blobClient ?? createBlobClient(config, { logger: createLogger('sequencer:blob-client:client') });
|
|
190
|
+
this.blobClient = deps.blobClient;
|
|
168
191
|
|
|
169
192
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
170
193
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
194
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
171
195
|
this.l1TxUtils = deps.l1TxUtils;
|
|
196
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
172
197
|
|
|
173
198
|
this.rollupContract = deps.rollupContract;
|
|
174
199
|
|
|
@@ -190,12 +215,52 @@ export class SequencerPublisher {
|
|
|
190
215
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
191
216
|
);
|
|
192
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
|
+
});
|
|
193
250
|
}
|
|
194
251
|
|
|
195
252
|
public getRollupContract(): RollupContract {
|
|
196
253
|
return this.rollupContract;
|
|
197
254
|
}
|
|
198
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
|
+
|
|
199
264
|
public getSenderAddress() {
|
|
200
265
|
return this.l1TxUtils.getSenderAddress();
|
|
201
266
|
}
|
|
@@ -271,7 +336,7 @@ export class SequencerPublisher {
|
|
|
271
336
|
// Start the analysis
|
|
272
337
|
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
273
338
|
l2SlotNumber,
|
|
274
|
-
gasLimit > 0n ? gasLimit :
|
|
339
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
275
340
|
l1Requests,
|
|
276
341
|
blobConfig,
|
|
277
342
|
onComplete,
|
|
@@ -297,6 +362,7 @@ export class SequencerPublisher {
|
|
|
297
362
|
* - a receipt and errorMsg if it failed on L1
|
|
298
363
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
299
364
|
*/
|
|
365
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
300
366
|
public async sendRequests() {
|
|
301
367
|
const requestsToProcess = [...this.requests];
|
|
302
368
|
this.requests = [];
|
|
@@ -343,7 +409,16 @@ export class SequencerPublisher {
|
|
|
343
409
|
|
|
344
410
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
345
411
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
346
|
-
|
|
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
|
+
}
|
|
347
422
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
348
423
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
349
424
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -353,19 +428,36 @@ export class SequencerPublisher {
|
|
|
353
428
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
354
429
|
|
|
355
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
|
+
|
|
356
448
|
this.log.debug('Forwarding transactions', {
|
|
357
449
|
validRequests: validRequests.map(request => request.action),
|
|
358
450
|
txConfig,
|
|
359
451
|
});
|
|
360
|
-
const result = await
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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,
|
|
367
460
|
);
|
|
368
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
369
461
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
370
462
|
} catch (err) {
|
|
371
463
|
const viemError = formatViemError(err);
|
|
@@ -383,13 +475,76 @@ export class SequencerPublisher {
|
|
|
383
475
|
}
|
|
384
476
|
}
|
|
385
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
|
+
|
|
386
527
|
private callbackBundledTransactions(
|
|
387
528
|
requests: RequestWithExpiry[],
|
|
388
|
-
result
|
|
529
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
530
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
389
531
|
) {
|
|
390
532
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
391
533
|
if (result instanceof FormattedViemError) {
|
|
392
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
|
+
});
|
|
393
548
|
return { failedActions: requests.map(r => r.action) };
|
|
394
549
|
} else {
|
|
395
550
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -402,6 +557,30 @@ export class SequencerPublisher {
|
|
|
402
557
|
failedActions.push(request.action);
|
|
403
558
|
}
|
|
404
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
|
+
}
|
|
405
584
|
return { successfulActions, failedActions };
|
|
406
585
|
}
|
|
407
586
|
}
|
|
@@ -414,17 +593,14 @@ export class SequencerPublisher {
|
|
|
414
593
|
public canProposeAtNextEthBlock(
|
|
415
594
|
tipArchive: Fr,
|
|
416
595
|
msgSender: EthAddress,
|
|
417
|
-
opts: {
|
|
596
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
418
597
|
) {
|
|
419
598
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
420
599
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
421
600
|
|
|
422
601
|
return this.rollupContract
|
|
423
602
|
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
424
|
-
forcePendingCheckpointNumber:
|
|
425
|
-
opts.forcePendingBlockNumber !== undefined
|
|
426
|
-
? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
|
|
427
|
-
: undefined,
|
|
603
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
428
604
|
})
|
|
429
605
|
.catch(err => {
|
|
430
606
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
@@ -443,10 +619,11 @@ export class SequencerPublisher {
|
|
|
443
619
|
* It will throw if the block header is invalid.
|
|
444
620
|
* @param header - The block header to validate
|
|
445
621
|
*/
|
|
622
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
446
623
|
public async validateBlockHeader(
|
|
447
624
|
header: CheckpointHeader,
|
|
448
|
-
opts?: {
|
|
449
|
-
) {
|
|
625
|
+
opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
|
|
626
|
+
): Promise<void> {
|
|
450
627
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
451
628
|
|
|
452
629
|
const args = [
|
|
@@ -455,17 +632,13 @@ export class SequencerPublisher {
|
|
|
455
632
|
[], // no signers
|
|
456
633
|
Signature.empty().toViemSignature(),
|
|
457
634
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
458
|
-
header.
|
|
635
|
+
header.blobsHash.toString(),
|
|
459
636
|
flags,
|
|
460
637
|
] as const;
|
|
461
638
|
|
|
462
639
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
463
|
-
const optsForcePendingCheckpointNumber =
|
|
464
|
-
opts?.forcePendingBlockNumber !== undefined
|
|
465
|
-
? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
|
|
466
|
-
: undefined;
|
|
467
640
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
468
|
-
|
|
641
|
+
opts?.forcePendingCheckpointNumber,
|
|
469
642
|
);
|
|
470
643
|
let balance = 0n;
|
|
471
644
|
if (this.config.fishermanMode) {
|
|
@@ -493,77 +666,109 @@ export class SequencerPublisher {
|
|
|
493
666
|
}
|
|
494
667
|
|
|
495
668
|
/**
|
|
496
|
-
* Simulate making a call to invalidate a
|
|
497
|
-
* @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)
|
|
498
671
|
*/
|
|
499
|
-
public async
|
|
500
|
-
validationResult:
|
|
501
|
-
): Promise<
|
|
672
|
+
public async simulateInvalidateCheckpoint(
|
|
673
|
+
validationResult: ValidateCheckpointResult,
|
|
674
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
502
675
|
if (validationResult.valid) {
|
|
503
676
|
return undefined;
|
|
504
677
|
}
|
|
505
678
|
|
|
506
|
-
const { reason,
|
|
507
|
-
const
|
|
508
|
-
const logData = { ...
|
|
679
|
+
const { reason, checkpoint } = validationResult;
|
|
680
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
681
|
+
const logData = { ...checkpoint, reason };
|
|
509
682
|
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
683
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
684
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
512
685
|
this.log.verbose(
|
|
513
|
-
`Skipping
|
|
514
|
-
{
|
|
686
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
687
|
+
{ currentCheckpointNumber, ...logData },
|
|
515
688
|
);
|
|
516
689
|
return undefined;
|
|
517
690
|
}
|
|
518
691
|
|
|
519
|
-
const request = this.
|
|
520
|
-
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();
|
|
521
696
|
|
|
522
697
|
try {
|
|
523
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
524
|
-
|
|
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
|
+
});
|
|
525
709
|
|
|
526
|
-
return {
|
|
710
|
+
return {
|
|
711
|
+
request,
|
|
712
|
+
gasUsed,
|
|
713
|
+
checkpointNumber,
|
|
714
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
715
|
+
reason,
|
|
716
|
+
};
|
|
527
717
|
} catch (err) {
|
|
528
718
|
const viemError = formatViemError(err);
|
|
529
719
|
|
|
530
|
-
// If the error is due to the
|
|
531
|
-
// we can safely ignore it and return undefined so we go ahead with
|
|
532
|
-
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')) {
|
|
533
723
|
this.log.verbose(
|
|
534
|
-
`Simulation for invalidate
|
|
724
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
535
725
|
{ ...logData, request, error: viemError.message },
|
|
536
726
|
);
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
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 });
|
|
540
730
|
return undefined;
|
|
541
731
|
} else {
|
|
542
732
|
this.log.error(
|
|
543
|
-
`Simulation for invalidate ${
|
|
733
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
544
734
|
viemError,
|
|
545
735
|
logData,
|
|
546
736
|
);
|
|
547
|
-
throw new Error(
|
|
548
|
-
|
|
549
|
-
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
739
|
+
{
|
|
740
|
+
cause: viemError,
|
|
741
|
+
},
|
|
742
|
+
);
|
|
550
743
|
}
|
|
551
744
|
}
|
|
552
745
|
|
|
553
|
-
// Otherwise, throw. We cannot build the next
|
|
554
|
-
this.log.error(`Simulation for invalidate
|
|
555
|
-
|
|
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 });
|
|
556
761
|
}
|
|
557
762
|
}
|
|
558
763
|
|
|
559
|
-
private
|
|
764
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
560
765
|
if (validationResult.valid) {
|
|
561
|
-
throw new Error('Cannot invalidate a valid
|
|
766
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
562
767
|
}
|
|
563
768
|
|
|
564
|
-
const {
|
|
565
|
-
const logData = { ...
|
|
566
|
-
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);
|
|
567
772
|
|
|
568
773
|
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
569
774
|
validationResult.attestations,
|
|
@@ -571,14 +776,14 @@ export class SequencerPublisher {
|
|
|
571
776
|
|
|
572
777
|
if (reason === 'invalid-attestation') {
|
|
573
778
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
574
|
-
|
|
779
|
+
checkpoint.checkpointNumber,
|
|
575
780
|
attestationsAndSigners,
|
|
576
781
|
committee,
|
|
577
782
|
validationResult.invalidIndex,
|
|
578
783
|
);
|
|
579
784
|
} else if (reason === 'insufficient-attestations') {
|
|
580
785
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
581
|
-
|
|
786
|
+
checkpoint.checkpointNumber,
|
|
582
787
|
attestationsAndSigners,
|
|
583
788
|
committee,
|
|
584
789
|
);
|
|
@@ -589,31 +794,16 @@ export class SequencerPublisher {
|
|
|
589
794
|
}
|
|
590
795
|
|
|
591
796
|
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
797
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
592
798
|
public async validateCheckpointForSubmission(
|
|
593
799
|
checkpoint: Checkpoint,
|
|
594
800
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
595
801
|
attestationsAndSignersSignature: Signature,
|
|
596
|
-
options: {
|
|
802
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
597
803
|
): Promise<bigint> {
|
|
598
804
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
599
|
-
|
|
600
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
601
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
602
|
-
// so that the committee is recalculated correctly
|
|
603
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
604
|
-
// if (ignoreSignatures) {
|
|
605
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
606
|
-
// if (!committee) {
|
|
607
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
608
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
609
|
-
// }
|
|
610
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
611
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
612
|
-
// );
|
|
613
|
-
// }
|
|
614
|
-
|
|
615
805
|
const blobFields = checkpoint.toBlobFields();
|
|
616
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
806
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
617
807
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
618
808
|
|
|
619
809
|
const args = [
|
|
@@ -621,7 +811,7 @@ export class SequencerPublisher {
|
|
|
621
811
|
header: checkpoint.header.toViem(),
|
|
622
812
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
623
813
|
oracleInput: {
|
|
624
|
-
feeAssetPriceModifier:
|
|
814
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
625
815
|
},
|
|
626
816
|
},
|
|
627
817
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -670,6 +860,32 @@ export class SequencerPublisher {
|
|
|
670
860
|
return false;
|
|
671
861
|
}
|
|
672
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
|
+
|
|
673
889
|
const cachedLastVote = this.lastActions[signalType];
|
|
674
890
|
this.lastActions[signalType] = slotNumber;
|
|
675
891
|
const action = signalType;
|
|
@@ -688,11 +904,26 @@ export class SequencerPublisher {
|
|
|
688
904
|
lastValidL2Slot: slotNumber,
|
|
689
905
|
});
|
|
690
906
|
|
|
907
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
908
|
+
|
|
691
909
|
try {
|
|
692
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
910
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
693
911
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
694
912
|
} catch (err) {
|
|
695
|
-
|
|
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
|
+
});
|
|
696
927
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
697
928
|
}
|
|
698
929
|
|
|
@@ -892,19 +1123,20 @@ export class SequencerPublisher {
|
|
|
892
1123
|
checkpoint: Checkpoint,
|
|
893
1124
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
894
1125
|
attestationsAndSignersSignature: Signature,
|
|
895
|
-
opts: { txTimeoutAt?: Date;
|
|
1126
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
896
1127
|
): Promise<void> {
|
|
897
1128
|
const checkpointHeader = checkpoint.header;
|
|
898
1129
|
|
|
899
1130
|
const blobFields = checkpoint.toBlobFields();
|
|
900
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1131
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
901
1132
|
|
|
902
|
-
const proposeTxArgs = {
|
|
1133
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
903
1134
|
header: checkpointHeader,
|
|
904
1135
|
archive: checkpoint.archive.root.toBuffer(),
|
|
905
1136
|
blobs,
|
|
906
1137
|
attestationsAndSigners,
|
|
907
1138
|
attestationsAndSignersSignature,
|
|
1139
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
908
1140
|
};
|
|
909
1141
|
|
|
910
1142
|
let ts: bigint;
|
|
@@ -925,7 +1157,7 @@ export class SequencerPublisher {
|
|
|
925
1157
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
926
1158
|
...checkpoint.getStats(),
|
|
927
1159
|
slotNumber: checkpoint.header.slotNumber,
|
|
928
|
-
|
|
1160
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
929
1161
|
});
|
|
930
1162
|
throw err;
|
|
931
1163
|
}
|
|
@@ -934,7 +1166,10 @@ export class SequencerPublisher {
|
|
|
934
1166
|
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
935
1167
|
}
|
|
936
1168
|
|
|
937
|
-
public
|
|
1169
|
+
public enqueueInvalidateCheckpoint(
|
|
1170
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
1171
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
1172
|
+
) {
|
|
938
1173
|
if (!request) {
|
|
939
1174
|
return;
|
|
940
1175
|
}
|
|
@@ -942,9 +1177,9 @@ export class SequencerPublisher {
|
|
|
942
1177
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
943
1178
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
944
1179
|
|
|
945
|
-
const { gasUsed,
|
|
946
|
-
const logData = { gasUsed,
|
|
947
|
-
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);
|
|
948
1183
|
this.addRequest({
|
|
949
1184
|
action: `invalidate-by-${request.reason}`,
|
|
950
1185
|
request: request.request,
|
|
@@ -957,9 +1192,9 @@ export class SequencerPublisher {
|
|
|
957
1192
|
result.receipt.status === 'success' &&
|
|
958
1193
|
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
959
1194
|
if (!success) {
|
|
960
|
-
this.log.warn(`Invalidate
|
|
1195
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
961
1196
|
} else {
|
|
962
|
-
this.log.info(`Invalidate
|
|
1197
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
963
1198
|
}
|
|
964
1199
|
return !!success;
|
|
965
1200
|
},
|
|
@@ -984,13 +1219,30 @@ export class SequencerPublisher {
|
|
|
984
1219
|
|
|
985
1220
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
986
1221
|
|
|
1222
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1223
|
+
|
|
987
1224
|
let gasUsed: bigint;
|
|
1225
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
988
1226
|
try {
|
|
989
|
-
({ 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
|
|
990
1228
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
991
1229
|
} catch (err) {
|
|
992
|
-
const viemError = formatViemError(err);
|
|
1230
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
993
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
|
+
|
|
994
1246
|
return false;
|
|
995
1247
|
}
|
|
996
1248
|
|
|
@@ -998,10 +1250,14 @@ export class SequencerPublisher {
|
|
|
998
1250
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
999
1251
|
logData.gasLimit = gasLimit;
|
|
1000
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
|
+
|
|
1001
1257
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1002
1258
|
this.addRequest({
|
|
1003
1259
|
action,
|
|
1004
|
-
request,
|
|
1260
|
+
request: requestWithAbi,
|
|
1005
1261
|
gasConfig: { gasLimit },
|
|
1006
1262
|
lastValidL2Slot: slotNumber,
|
|
1007
1263
|
checkSuccess: (_req, result) => {
|
|
@@ -1038,7 +1294,7 @@ export class SequencerPublisher {
|
|
|
1038
1294
|
private async prepareProposeTx(
|
|
1039
1295
|
encodedData: L1ProcessArgs,
|
|
1040
1296
|
timestamp: bigint,
|
|
1041
|
-
options: {
|
|
1297
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1042
1298
|
) {
|
|
1043
1299
|
const kzg = Blob.getViemKzgInstance();
|
|
1044
1300
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
@@ -1070,9 +1326,27 @@ export class SequencerPublisher {
|
|
|
1070
1326
|
kzg,
|
|
1071
1327
|
},
|
|
1072
1328
|
)
|
|
1073
|
-
.catch(err => {
|
|
1074
|
-
const
|
|
1075
|
-
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
|
+
});
|
|
1076
1350
|
throw new Error('Failed to validate blobs');
|
|
1077
1351
|
});
|
|
1078
1352
|
}
|
|
@@ -1083,8 +1357,7 @@ export class SequencerPublisher {
|
|
|
1083
1357
|
header: encodedData.header.toViem(),
|
|
1084
1358
|
archive: toHex(encodedData.archive),
|
|
1085
1359
|
oracleInput: {
|
|
1086
|
-
|
|
1087
|
-
feeAssetPriceModifier: 0n,
|
|
1360
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1088
1361
|
},
|
|
1089
1362
|
},
|
|
1090
1363
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1110,7 +1383,7 @@ export class SequencerPublisher {
|
|
|
1110
1383
|
readonly header: ViemHeader;
|
|
1111
1384
|
readonly archive: `0x${string}`;
|
|
1112
1385
|
readonly oracleInput: {
|
|
1113
|
-
readonly feeAssetPriceModifier:
|
|
1386
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1114
1387
|
};
|
|
1115
1388
|
},
|
|
1116
1389
|
ViemCommitteeAttestations,
|
|
@@ -1119,7 +1392,7 @@ export class SequencerPublisher {
|
|
|
1119
1392
|
`0x${string}`,
|
|
1120
1393
|
],
|
|
1121
1394
|
timestamp: bigint,
|
|
1122
|
-
options: {
|
|
1395
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1123
1396
|
) {
|
|
1124
1397
|
const rollupData = encodeFunctionData({
|
|
1125
1398
|
abi: RollupAbi,
|
|
@@ -1128,13 +1401,9 @@ export class SequencerPublisher {
|
|
|
1128
1401
|
});
|
|
1129
1402
|
|
|
1130
1403
|
// override the pending checkpoint number if requested
|
|
1131
|
-
const optsForcePendingCheckpointNumber =
|
|
1132
|
-
options.forcePendingBlockNumber !== undefined
|
|
1133
|
-
? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
|
|
1134
|
-
: undefined;
|
|
1135
1404
|
const forcePendingCheckpointNumberStateDiff = (
|
|
1136
|
-
|
|
1137
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
1405
|
+
options.forcePendingCheckpointNumber !== undefined
|
|
1406
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1138
1407
|
: []
|
|
1139
1408
|
).flatMap(override => override.stateDiff ?? []);
|
|
1140
1409
|
|
|
@@ -1156,25 +1425,27 @@ export class SequencerPublisher {
|
|
|
1156
1425
|
});
|
|
1157
1426
|
}
|
|
1158
1427
|
|
|
1428
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1429
|
+
|
|
1159
1430
|
const simulationResult = await this.l1TxUtils
|
|
1160
1431
|
.simulate(
|
|
1161
1432
|
{
|
|
1162
1433
|
to: this.rollupContract.address,
|
|
1163
1434
|
data: rollupData,
|
|
1164
|
-
gas:
|
|
1435
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1165
1436
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1166
1437
|
},
|
|
1167
1438
|
{
|
|
1168
1439
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1169
1440
|
time: timestamp + 1n,
|
|
1170
1441
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1171
|
-
gasLimit:
|
|
1442
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1172
1443
|
},
|
|
1173
1444
|
stateOverrides,
|
|
1174
1445
|
RollupAbi,
|
|
1175
1446
|
{
|
|
1176
1447
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1177
|
-
fallbackGasEstimate:
|
|
1448
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1178
1449
|
},
|
|
1179
1450
|
)
|
|
1180
1451
|
.catch(err => {
|
|
@@ -1184,11 +1455,23 @@ export class SequencerPublisher {
|
|
|
1184
1455
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1185
1456
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1186
1457
|
return {
|
|
1187
|
-
gasUsed:
|
|
1458
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1188
1459
|
logs: [],
|
|
1189
1460
|
};
|
|
1190
1461
|
}
|
|
1191
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
|
+
});
|
|
1192
1475
|
throw err;
|
|
1193
1476
|
});
|
|
1194
1477
|
|
|
@@ -1198,7 +1481,7 @@ export class SequencerPublisher {
|
|
|
1198
1481
|
private async addProposeTx(
|
|
1199
1482
|
checkpoint: Checkpoint,
|
|
1200
1483
|
encodedData: L1ProcessArgs,
|
|
1201
|
-
opts: { txTimeoutAt?: Date;
|
|
1484
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1202
1485
|
timestamp: bigint,
|
|
1203
1486
|
): Promise<void> {
|
|
1204
1487
|
const slot = checkpoint.header.slotNumber;
|