@aztec/sequencer-client 0.0.1-commit.3469e52 → 0.0.1-commit.3895657bc
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 +23 -7
- 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 +40 -30
- package/dest/global_variable_builder/global_builder.d.ts +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +2 -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 +11 -3
- 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 +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +26 -8
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +338 -48
- package/dest/sequencer/checkpoint_proposal_job.d.ts +31 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +180 -95
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +111 -30
- package/dest/sequencer/sequencer.d.ts +25 -12
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +31 -28
- 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 +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +17 -14
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +63 -40
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +10 -9
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +55 -41
- package/src/global_variable_builder/global_builder.ts +3 -3
- 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 +38 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +333 -60
- package/src/sequencer/checkpoint_proposal_job.ts +246 -127
- package/src/sequencer/metrics.ts +124 -32
- package/src/sequencer/sequencer.ts +41 -33
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +4 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +90 -62
- package/src/test/utils.ts +22 -13
|
@@ -4,6 +4,7 @@ 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,19 +19,22 @@ 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
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';
|
|
@@ -42,9 +46,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
42
46
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
43
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 = [
|
|
@@ -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,7 +153,7 @@ 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;
|
|
@@ -144,11 +164,12 @@ export class SequencerPublisher {
|
|
|
144
164
|
protected requests: RequestWithExpiry[] = [];
|
|
145
165
|
|
|
146
166
|
constructor(
|
|
147
|
-
private config:
|
|
167
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
168
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
148
169
|
deps: {
|
|
149
170
|
telemetry?: TelemetryClient;
|
|
150
171
|
blobClient: BlobClientInterface;
|
|
151
|
-
l1TxUtils:
|
|
172
|
+
l1TxUtils: L1TxUtils;
|
|
152
173
|
rollupContract: RollupContract;
|
|
153
174
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
154
175
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -158,6 +179,7 @@ export class SequencerPublisher {
|
|
|
158
179
|
metrics: SequencerPublisherMetrics;
|
|
159
180
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
160
181
|
log?: Logger;
|
|
182
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
161
183
|
},
|
|
162
184
|
) {
|
|
163
185
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -171,6 +193,7 @@ export class SequencerPublisher {
|
|
|
171
193
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
172
194
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
173
195
|
this.l1TxUtils = deps.l1TxUtils;
|
|
196
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
174
197
|
|
|
175
198
|
this.rollupContract = deps.rollupContract;
|
|
176
199
|
|
|
@@ -192,12 +215,52 @@ export class SequencerPublisher {
|
|
|
192
215
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
193
216
|
);
|
|
194
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
|
+
});
|
|
195
250
|
}
|
|
196
251
|
|
|
197
252
|
public getRollupContract(): RollupContract {
|
|
198
253
|
return this.rollupContract;
|
|
199
254
|
}
|
|
200
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
|
+
|
|
201
264
|
public getSenderAddress() {
|
|
202
265
|
return this.l1TxUtils.getSenderAddress();
|
|
203
266
|
}
|
|
@@ -273,7 +336,7 @@ export class SequencerPublisher {
|
|
|
273
336
|
// Start the analysis
|
|
274
337
|
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
275
338
|
l2SlotNumber,
|
|
276
|
-
gasLimit > 0n ? gasLimit :
|
|
339
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
277
340
|
l1Requests,
|
|
278
341
|
blobConfig,
|
|
279
342
|
onComplete,
|
|
@@ -346,7 +409,16 @@ export class SequencerPublisher {
|
|
|
346
409
|
|
|
347
410
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
348
411
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
349
|
-
|
|
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
|
+
}
|
|
350
422
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
351
423
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
352
424
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -356,19 +428,36 @@ export class SequencerPublisher {
|
|
|
356
428
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
357
429
|
|
|
358
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
|
+
|
|
359
448
|
this.log.debug('Forwarding transactions', {
|
|
360
449
|
validRequests: validRequests.map(request => request.action),
|
|
361
450
|
txConfig,
|
|
362
451
|
});
|
|
363
|
-
const result = await
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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,
|
|
370
460
|
);
|
|
371
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
372
461
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
373
462
|
} catch (err) {
|
|
374
463
|
const viemError = formatViemError(err);
|
|
@@ -386,13 +475,76 @@ export class SequencerPublisher {
|
|
|
386
475
|
}
|
|
387
476
|
}
|
|
388
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
|
+
|
|
389
527
|
private callbackBundledTransactions(
|
|
390
528
|
requests: RequestWithExpiry[],
|
|
391
|
-
result
|
|
529
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
530
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
392
531
|
) {
|
|
393
532
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
394
533
|
if (result instanceof FormattedViemError) {
|
|
395
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
|
+
});
|
|
396
548
|
return { failedActions: requests.map(r => r.action) };
|
|
397
549
|
} else {
|
|
398
550
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -405,6 +557,30 @@ export class SequencerPublisher {
|
|
|
405
557
|
failedActions.push(request.action);
|
|
406
558
|
}
|
|
407
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
|
+
}
|
|
408
584
|
return { successfulActions, failedActions };
|
|
409
585
|
}
|
|
410
586
|
}
|
|
@@ -516,8 +692,15 @@ export class SequencerPublisher {
|
|
|
516
692
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
517
693
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
518
694
|
|
|
695
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
696
|
+
|
|
519
697
|
try {
|
|
520
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
698
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
699
|
+
request,
|
|
700
|
+
undefined,
|
|
701
|
+
undefined,
|
|
702
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
703
|
+
);
|
|
521
704
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
522
705
|
...logData,
|
|
523
706
|
request,
|
|
@@ -536,7 +719,7 @@ export class SequencerPublisher {
|
|
|
536
719
|
|
|
537
720
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
538
721
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
539
|
-
if (viemError.message?.includes('
|
|
722
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
540
723
|
this.log.verbose(
|
|
541
724
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
542
725
|
{ ...logData, request, error: viemError.message },
|
|
@@ -562,6 +745,18 @@ export class SequencerPublisher {
|
|
|
562
745
|
|
|
563
746
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
564
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
|
+
});
|
|
565
760
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
566
761
|
}
|
|
567
762
|
}
|
|
@@ -607,24 +802,8 @@ export class SequencerPublisher {
|
|
|
607
802
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
608
803
|
): Promise<bigint> {
|
|
609
804
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
610
|
-
|
|
611
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
612
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
613
|
-
// so that the committee is recalculated correctly
|
|
614
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
615
|
-
// if (ignoreSignatures) {
|
|
616
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
617
|
-
// if (!committee) {
|
|
618
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
619
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
620
|
-
// }
|
|
621
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
622
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
623
|
-
// );
|
|
624
|
-
// }
|
|
625
|
-
|
|
626
805
|
const blobFields = checkpoint.toBlobFields();
|
|
627
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
806
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
628
807
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
629
808
|
|
|
630
809
|
const args = [
|
|
@@ -632,7 +811,7 @@ export class SequencerPublisher {
|
|
|
632
811
|
header: checkpoint.header.toViem(),
|
|
633
812
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
634
813
|
oracleInput: {
|
|
635
|
-
feeAssetPriceModifier:
|
|
814
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
636
815
|
},
|
|
637
816
|
},
|
|
638
817
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -681,6 +860,32 @@ export class SequencerPublisher {
|
|
|
681
860
|
return false;
|
|
682
861
|
}
|
|
683
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
|
+
|
|
684
889
|
const cachedLastVote = this.lastActions[signalType];
|
|
685
890
|
this.lastActions[signalType] = slotNumber;
|
|
686
891
|
const action = signalType;
|
|
@@ -699,11 +904,26 @@ export class SequencerPublisher {
|
|
|
699
904
|
lastValidL2Slot: slotNumber,
|
|
700
905
|
});
|
|
701
906
|
|
|
907
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
908
|
+
|
|
702
909
|
try {
|
|
703
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
910
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
704
911
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
705
912
|
} catch (err) {
|
|
706
|
-
|
|
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
|
+
});
|
|
707
927
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
708
928
|
}
|
|
709
929
|
|
|
@@ -908,14 +1128,15 @@ export class SequencerPublisher {
|
|
|
908
1128
|
const checkpointHeader = checkpoint.header;
|
|
909
1129
|
|
|
910
1130
|
const blobFields = checkpoint.toBlobFields();
|
|
911
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1131
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
912
1132
|
|
|
913
|
-
const proposeTxArgs = {
|
|
1133
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
914
1134
|
header: checkpointHeader,
|
|
915
1135
|
archive: checkpoint.archive.root.toBuffer(),
|
|
916
1136
|
blobs,
|
|
917
1137
|
attestationsAndSigners,
|
|
918
1138
|
attestationsAndSignersSignature,
|
|
1139
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
919
1140
|
};
|
|
920
1141
|
|
|
921
1142
|
let ts: bigint;
|
|
@@ -998,13 +1219,30 @@ export class SequencerPublisher {
|
|
|
998
1219
|
|
|
999
1220
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1000
1221
|
|
|
1222
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1223
|
+
|
|
1001
1224
|
let gasUsed: bigint;
|
|
1225
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1002
1226
|
try {
|
|
1003
|
-
({ 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
|
|
1004
1228
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1005
1229
|
} catch (err) {
|
|
1006
|
-
const viemError = formatViemError(err);
|
|
1230
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1007
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
|
+
|
|
1008
1246
|
return false;
|
|
1009
1247
|
}
|
|
1010
1248
|
|
|
@@ -1012,10 +1250,14 @@ export class SequencerPublisher {
|
|
|
1012
1250
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1013
1251
|
logData.gasLimit = gasLimit;
|
|
1014
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
|
+
|
|
1015
1257
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1016
1258
|
this.addRequest({
|
|
1017
1259
|
action,
|
|
1018
|
-
request,
|
|
1260
|
+
request: requestWithAbi,
|
|
1019
1261
|
gasConfig: { gasLimit },
|
|
1020
1262
|
lastValidL2Slot: slotNumber,
|
|
1021
1263
|
checkSuccess: (_req, result) => {
|
|
@@ -1084,9 +1326,27 @@ export class SequencerPublisher {
|
|
|
1084
1326
|
kzg,
|
|
1085
1327
|
},
|
|
1086
1328
|
)
|
|
1087
|
-
.catch(err => {
|
|
1088
|
-
const
|
|
1089
|
-
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
|
+
});
|
|
1090
1350
|
throw new Error('Failed to validate blobs');
|
|
1091
1351
|
});
|
|
1092
1352
|
}
|
|
@@ -1097,8 +1357,7 @@ export class SequencerPublisher {
|
|
|
1097
1357
|
header: encodedData.header.toViem(),
|
|
1098
1358
|
archive: toHex(encodedData.archive),
|
|
1099
1359
|
oracleInput: {
|
|
1100
|
-
|
|
1101
|
-
feeAssetPriceModifier: 0n,
|
|
1360
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1102
1361
|
},
|
|
1103
1362
|
},
|
|
1104
1363
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1124,7 +1383,7 @@ export class SequencerPublisher {
|
|
|
1124
1383
|
readonly header: ViemHeader;
|
|
1125
1384
|
readonly archive: `0x${string}`;
|
|
1126
1385
|
readonly oracleInput: {
|
|
1127
|
-
readonly feeAssetPriceModifier:
|
|
1386
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1128
1387
|
};
|
|
1129
1388
|
},
|
|
1130
1389
|
ViemCommitteeAttestations,
|
|
@@ -1166,25 +1425,27 @@ export class SequencerPublisher {
|
|
|
1166
1425
|
});
|
|
1167
1426
|
}
|
|
1168
1427
|
|
|
1428
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1429
|
+
|
|
1169
1430
|
const simulationResult = await this.l1TxUtils
|
|
1170
1431
|
.simulate(
|
|
1171
1432
|
{
|
|
1172
1433
|
to: this.rollupContract.address,
|
|
1173
1434
|
data: rollupData,
|
|
1174
|
-
gas:
|
|
1435
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1175
1436
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1176
1437
|
},
|
|
1177
1438
|
{
|
|
1178
1439
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1179
1440
|
time: timestamp + 1n,
|
|
1180
1441
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1181
|
-
gasLimit:
|
|
1442
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1182
1443
|
},
|
|
1183
1444
|
stateOverrides,
|
|
1184
1445
|
RollupAbi,
|
|
1185
1446
|
{
|
|
1186
1447
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1187
|
-
fallbackGasEstimate:
|
|
1448
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1188
1449
|
},
|
|
1189
1450
|
)
|
|
1190
1451
|
.catch(err => {
|
|
@@ -1194,11 +1455,23 @@ export class SequencerPublisher {
|
|
|
1194
1455
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1195
1456
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1196
1457
|
return {
|
|
1197
|
-
gasUsed:
|
|
1458
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1198
1459
|
logs: [],
|
|
1199
1460
|
};
|
|
1200
1461
|
}
|
|
1201
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
|
+
});
|
|
1202
1475
|
throw err;
|
|
1203
1476
|
});
|
|
1204
1477
|
|