@aztec/sequencer-client 0.0.1-commit.87a0206 → 0.0.1-commit.88c5703d4
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 +24 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +101 -16
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +49 -28
- 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 +5 -4
- 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.d.ts +26 -7
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +310 -31
- package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +122 -73
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +86 -15
- package/dest/sequencer/sequencer.d.ts +26 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +36 -39
- 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 +2 -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 +14 -10
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +64 -38
- package/src/global_variable_builder/global_builder.ts +4 -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.ts +311 -44
- package/src/sequencer/checkpoint_proposal_job.ts +167 -77
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +45 -45
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +62 -48
- package/src/test/utils.ts +5 -2
|
@@ -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,20 +19,23 @@ import {
|
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
21
23
|
MAX_L1_TX_LIMIT,
|
|
22
24
|
type TransactionStats,
|
|
23
25
|
WEI_CONST,
|
|
24
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
27
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
28
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
28
29
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
29
30
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
31
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
30
32
|
import { pick } from '@aztec/foundation/collection';
|
|
31
33
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
34
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
32
35
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
36
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
34
37
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
38
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
35
39
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
40
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
41
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
@@ -43,9 +47,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
43
47
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
45
49
|
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
import {
|
|
51
|
+
type Hex,
|
|
52
|
+
type StateOverride,
|
|
53
|
+
type TransactionReceipt,
|
|
54
|
+
type TypedDataDefinition,
|
|
55
|
+
encodeFunctionData,
|
|
56
|
+
keccak256,
|
|
57
|
+
multicall3Abi,
|
|
58
|
+
toHex,
|
|
59
|
+
} from 'viem';
|
|
60
|
+
|
|
61
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
62
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
49
63
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
64
|
|
|
51
65
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -60,6 +74,8 @@ type L1ProcessArgs = {
|
|
|
60
74
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
75
|
/** Attestations and signers signature */
|
|
62
76
|
attestationsAndSignersSignature: Signature;
|
|
77
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
78
|
+
feeAssetPriceModifier: bigint;
|
|
63
79
|
};
|
|
64
80
|
|
|
65
81
|
export const Actions = [
|
|
@@ -105,6 +121,7 @@ export class SequencerPublisher {
|
|
|
105
121
|
private interrupted = false;
|
|
106
122
|
private metrics: SequencerPublisherMetrics;
|
|
107
123
|
public epochCache: EpochCache;
|
|
124
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
108
125
|
|
|
109
126
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
110
127
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -112,6 +129,7 @@ export class SequencerPublisher {
|
|
|
112
129
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
130
|
|
|
114
131
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
132
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
115
133
|
|
|
116
134
|
protected log: Logger;
|
|
117
135
|
protected ethereumSlotDuration: bigint;
|
|
@@ -121,15 +139,22 @@ export class SequencerPublisher {
|
|
|
121
139
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
122
140
|
private proposerAddressForSimulation?: EthAddress;
|
|
123
141
|
|
|
142
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
143
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
144
|
+
|
|
124
145
|
/** L1 fee analyzer for fisherman mode */
|
|
125
146
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
147
|
+
|
|
148
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
149
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
150
|
+
|
|
126
151
|
// A CALL to a cold address is 2700 gas
|
|
127
152
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
153
|
|
|
129
154
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
130
155
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
131
156
|
|
|
132
|
-
public l1TxUtils:
|
|
157
|
+
public l1TxUtils: L1TxUtils;
|
|
133
158
|
public rollupContract: RollupContract;
|
|
134
159
|
public govProposerContract: GovernanceProposerContract;
|
|
135
160
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -140,11 +165,12 @@ export class SequencerPublisher {
|
|
|
140
165
|
protected requests: RequestWithExpiry[] = [];
|
|
141
166
|
|
|
142
167
|
constructor(
|
|
143
|
-
private config:
|
|
168
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
169
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
144
170
|
deps: {
|
|
145
171
|
telemetry?: TelemetryClient;
|
|
146
172
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
173
|
+
l1TxUtils: L1TxUtils;
|
|
148
174
|
rollupContract: RollupContract;
|
|
149
175
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
150
176
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -154,6 +180,7 @@ export class SequencerPublisher {
|
|
|
154
180
|
metrics: SequencerPublisherMetrics;
|
|
155
181
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
156
182
|
log?: Logger;
|
|
183
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
157
184
|
},
|
|
158
185
|
) {
|
|
159
186
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -167,6 +194,7 @@ export class SequencerPublisher {
|
|
|
167
194
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
168
195
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
169
196
|
this.l1TxUtils = deps.l1TxUtils;
|
|
197
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
170
198
|
|
|
171
199
|
this.rollupContract = deps.rollupContract;
|
|
172
200
|
|
|
@@ -188,12 +216,52 @@ export class SequencerPublisher {
|
|
|
188
216
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
217
|
);
|
|
190
218
|
}
|
|
219
|
+
|
|
220
|
+
// Initialize fee asset price oracle
|
|
221
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
222
|
+
this.l1TxUtils.client,
|
|
223
|
+
this.rollupContract,
|
|
224
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
228
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
233
|
+
* Does nothing if no store is configured.
|
|
234
|
+
*/
|
|
235
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
236
|
+
if (!this.failedTxStore) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const tx: FailedL1Tx = {
|
|
241
|
+
...failedTx,
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Fire and forget - don't block on backup
|
|
246
|
+
void this.failedTxStore
|
|
247
|
+
.then(store => store?.saveFailedTx(tx))
|
|
248
|
+
.catch(err => {
|
|
249
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
250
|
+
});
|
|
191
251
|
}
|
|
192
252
|
|
|
193
253
|
public getRollupContract(): RollupContract {
|
|
194
254
|
return this.rollupContract;
|
|
195
255
|
}
|
|
196
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Gets the fee asset price modifier from the oracle.
|
|
259
|
+
* Returns 0n if the oracle query fails.
|
|
260
|
+
*/
|
|
261
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
262
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
263
|
+
}
|
|
264
|
+
|
|
197
265
|
public getSenderAddress() {
|
|
198
266
|
return this.l1TxUtils.getSenderAddress();
|
|
199
267
|
}
|
|
@@ -361,19 +429,36 @@ export class SequencerPublisher {
|
|
|
361
429
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
362
430
|
|
|
363
431
|
try {
|
|
432
|
+
// Capture context for failed tx backup before sending
|
|
433
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
434
|
+
const multicallData = encodeFunctionData({
|
|
435
|
+
abi: multicall3Abi,
|
|
436
|
+
functionName: 'aggregate3',
|
|
437
|
+
args: [
|
|
438
|
+
validRequests.map(r => ({
|
|
439
|
+
target: r.request.to!,
|
|
440
|
+
callData: r.request.data!,
|
|
441
|
+
allowFailure: true,
|
|
442
|
+
})),
|
|
443
|
+
],
|
|
444
|
+
});
|
|
445
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
446
|
+
|
|
447
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
448
|
+
|
|
364
449
|
this.log.debug('Forwarding transactions', {
|
|
365
450
|
validRequests: validRequests.map(request => request.action),
|
|
366
451
|
txConfig,
|
|
367
452
|
});
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
453
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
454
|
+
if (result === undefined) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
458
|
+
validRequests,
|
|
459
|
+
result,
|
|
460
|
+
txContext,
|
|
375
461
|
);
|
|
376
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
377
462
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
378
463
|
} catch (err) {
|
|
379
464
|
const viemError = formatViemError(err);
|
|
@@ -391,16 +476,88 @@ export class SequencerPublisher {
|
|
|
391
476
|
}
|
|
392
477
|
}
|
|
393
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
481
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
482
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
483
|
+
*/
|
|
484
|
+
private async forwardWithPublisherRotation(
|
|
485
|
+
validRequests: RequestWithExpiry[],
|
|
486
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
487
|
+
blobConfig: L1BlobInputs | undefined,
|
|
488
|
+
) {
|
|
489
|
+
const triedAddresses: EthAddress[] = [];
|
|
490
|
+
let currentPublisher = this.l1TxUtils;
|
|
491
|
+
|
|
492
|
+
while (true) {
|
|
493
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
494
|
+
try {
|
|
495
|
+
const result = await Multicall3.forward(
|
|
496
|
+
validRequests.map(r => r.request),
|
|
497
|
+
currentPublisher,
|
|
498
|
+
txConfig,
|
|
499
|
+
blobConfig,
|
|
500
|
+
this.rollupContract.address,
|
|
501
|
+
this.log,
|
|
502
|
+
);
|
|
503
|
+
this.l1TxUtils = currentPublisher;
|
|
504
|
+
return result;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
if (err instanceof TimeoutError) {
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
const viemError = formatViemError(err);
|
|
510
|
+
if (!this.getNextPublisher) {
|
|
511
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
this.log.warn(
|
|
515
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
516
|
+
viemError,
|
|
517
|
+
);
|
|
518
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
519
|
+
if (!nextPublisher) {
|
|
520
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
currentPublisher = nextPublisher;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
394
528
|
private callbackBundledTransactions(
|
|
395
529
|
requests: RequestWithExpiry[],
|
|
396
|
-
result
|
|
530
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
531
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
397
532
|
) {
|
|
398
533
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
399
534
|
if (result instanceof FormattedViemError) {
|
|
400
535
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
536
|
+
this.backupFailedTx({
|
|
537
|
+
id: keccak256(txContext.multicallData),
|
|
538
|
+
failureType: 'send-error',
|
|
539
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
540
|
+
blobData: txContext.blobData,
|
|
541
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
542
|
+
error: { message: result.message, name: result.name },
|
|
543
|
+
context: {
|
|
544
|
+
actions: requests.map(r => r.action),
|
|
545
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
546
|
+
sender: this.getSenderAddress().toString(),
|
|
547
|
+
},
|
|
548
|
+
});
|
|
401
549
|
return { failedActions: requests.map(r => r.action) };
|
|
402
550
|
} else {
|
|
403
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
551
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
552
|
+
result,
|
|
553
|
+
requests: requests.map(r => ({
|
|
554
|
+
...r,
|
|
555
|
+
// Avoid logging large blob data
|
|
556
|
+
blobConfig: r.blobConfig
|
|
557
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
558
|
+
: undefined,
|
|
559
|
+
})),
|
|
560
|
+
});
|
|
404
561
|
const successfulActions: Action[] = [];
|
|
405
562
|
const failedActions: Action[] = [];
|
|
406
563
|
for (const request of requests) {
|
|
@@ -410,6 +567,30 @@ export class SequencerPublisher {
|
|
|
410
567
|
failedActions.push(request.action);
|
|
411
568
|
}
|
|
412
569
|
}
|
|
570
|
+
// Single backup for the whole reverted tx
|
|
571
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
572
|
+
this.backupFailedTx({
|
|
573
|
+
id: result.receipt.transactionHash,
|
|
574
|
+
failureType: 'revert',
|
|
575
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
576
|
+
blobData: txContext.blobData,
|
|
577
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
578
|
+
receipt: {
|
|
579
|
+
transactionHash: result.receipt.transactionHash,
|
|
580
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
581
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
582
|
+
status: 'reverted',
|
|
583
|
+
},
|
|
584
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
585
|
+
context: {
|
|
586
|
+
actions: failedActions,
|
|
587
|
+
requests: requests
|
|
588
|
+
.filter(r => failedActions.includes(r.action))
|
|
589
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
590
|
+
sender: this.getSenderAddress().toString(),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
413
594
|
return { successfulActions, failedActions };
|
|
414
595
|
}
|
|
415
596
|
}
|
|
@@ -521,6 +702,8 @@ export class SequencerPublisher {
|
|
|
521
702
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
703
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
523
704
|
|
|
705
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
706
|
+
|
|
524
707
|
try {
|
|
525
708
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
709
|
request,
|
|
@@ -572,6 +755,18 @@ export class SequencerPublisher {
|
|
|
572
755
|
|
|
573
756
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
757
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
758
|
+
this.backupFailedTx({
|
|
759
|
+
id: keccak256(request.data!),
|
|
760
|
+
failureType: 'simulation',
|
|
761
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
762
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
763
|
+
error: { message: viemError.message, name: viemError.name },
|
|
764
|
+
context: {
|
|
765
|
+
actions: [`invalidate-${reason}`],
|
|
766
|
+
checkpointNumber,
|
|
767
|
+
sender: this.getSenderAddress().toString(),
|
|
768
|
+
},
|
|
769
|
+
});
|
|
575
770
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
576
771
|
}
|
|
577
772
|
}
|
|
@@ -617,24 +812,8 @@ export class SequencerPublisher {
|
|
|
617
812
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
618
813
|
): Promise<bigint> {
|
|
619
814
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
620
|
-
|
|
621
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
622
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
623
|
-
// so that the committee is recalculated correctly
|
|
624
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
625
|
-
// if (ignoreSignatures) {
|
|
626
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
627
|
-
// if (!committee) {
|
|
628
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
629
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
630
|
-
// }
|
|
631
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
632
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
633
|
-
// );
|
|
634
|
-
// }
|
|
635
|
-
|
|
636
815
|
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
816
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
638
817
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
818
|
|
|
640
819
|
const args = [
|
|
@@ -642,7 +821,7 @@ export class SequencerPublisher {
|
|
|
642
821
|
header: checkpoint.header.toViem(),
|
|
643
822
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
823
|
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier:
|
|
824
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
646
825
|
},
|
|
647
826
|
},
|
|
648
827
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -691,6 +870,32 @@ export class SequencerPublisher {
|
|
|
691
870
|
return false;
|
|
692
871
|
}
|
|
693
872
|
|
|
873
|
+
// Check if payload was already submitted to governance
|
|
874
|
+
const cacheKey = payload.toString();
|
|
875
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
876
|
+
try {
|
|
877
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
878
|
+
const proposed = await retry(
|
|
879
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
880
|
+
'Check if payload was proposed',
|
|
881
|
+
makeBackoff([0, 1, 2]),
|
|
882
|
+
this.log,
|
|
883
|
+
true,
|
|
884
|
+
);
|
|
885
|
+
if (proposed) {
|
|
886
|
+
this.payloadProposedCache.add(cacheKey);
|
|
887
|
+
}
|
|
888
|
+
} catch (err) {
|
|
889
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
895
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
|
|
694
899
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
900
|
this.lastActions[signalType] = slotNumber;
|
|
696
901
|
const action = signalType;
|
|
@@ -709,11 +914,26 @@ export class SequencerPublisher {
|
|
|
709
914
|
lastValidL2Slot: slotNumber,
|
|
710
915
|
});
|
|
711
916
|
|
|
917
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
918
|
+
|
|
712
919
|
try {
|
|
713
920
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
714
921
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
715
922
|
} catch (err) {
|
|
716
|
-
|
|
923
|
+
const viemError = formatViemError(err);
|
|
924
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
925
|
+
this.backupFailedTx({
|
|
926
|
+
id: keccak256(request.data!),
|
|
927
|
+
failureType: 'simulation',
|
|
928
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
929
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
930
|
+
error: { message: viemError.message, name: viemError.name },
|
|
931
|
+
context: {
|
|
932
|
+
actions: [action],
|
|
933
|
+
slot: slotNumber,
|
|
934
|
+
sender: this.getSenderAddress().toString(),
|
|
935
|
+
},
|
|
936
|
+
});
|
|
717
937
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
718
938
|
}
|
|
719
939
|
|
|
@@ -918,14 +1138,15 @@ export class SequencerPublisher {
|
|
|
918
1138
|
const checkpointHeader = checkpoint.header;
|
|
919
1139
|
|
|
920
1140
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1141
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
1142
|
|
|
923
|
-
const proposeTxArgs = {
|
|
1143
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
1144
|
header: checkpointHeader,
|
|
925
1145
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
1146
|
blobs,
|
|
927
1147
|
attestationsAndSigners,
|
|
928
1148
|
attestationsAndSignersSignature,
|
|
1149
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
1150
|
};
|
|
930
1151
|
|
|
931
1152
|
let ts: bigint;
|
|
@@ -1008,6 +1229,8 @@ export class SequencerPublisher {
|
|
|
1008
1229
|
|
|
1009
1230
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1010
1231
|
|
|
1232
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1233
|
+
|
|
1011
1234
|
let gasUsed: bigint;
|
|
1012
1235
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1013
1236
|
try {
|
|
@@ -1017,6 +1240,19 @@ export class SequencerPublisher {
|
|
|
1017
1240
|
const viemError = formatViemError(err, simulateAbi);
|
|
1018
1241
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
1242
|
|
|
1243
|
+
this.backupFailedTx({
|
|
1244
|
+
id: keccak256(request.data!),
|
|
1245
|
+
failureType: 'simulation',
|
|
1246
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1247
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1248
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1249
|
+
context: {
|
|
1250
|
+
actions: [action],
|
|
1251
|
+
slot: slotNumber,
|
|
1252
|
+
sender: this.getSenderAddress().toString(),
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1020
1256
|
return false;
|
|
1021
1257
|
}
|
|
1022
1258
|
|
|
@@ -1100,9 +1336,27 @@ export class SequencerPublisher {
|
|
|
1100
1336
|
kzg,
|
|
1101
1337
|
},
|
|
1102
1338
|
)
|
|
1103
|
-
.catch(err => {
|
|
1104
|
-
const
|
|
1105
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1339
|
+
.catch(async err => {
|
|
1340
|
+
const viemError = formatViemError(err);
|
|
1341
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1342
|
+
const validateBlobsData = encodeFunctionData({
|
|
1343
|
+
abi: RollupAbi,
|
|
1344
|
+
functionName: 'validateBlobs',
|
|
1345
|
+
args: [blobInput],
|
|
1346
|
+
});
|
|
1347
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1348
|
+
this.backupFailedTx({
|
|
1349
|
+
id: keccak256(validateBlobsData),
|
|
1350
|
+
failureType: 'simulation',
|
|
1351
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1352
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1353
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1354
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1355
|
+
context: {
|
|
1356
|
+
actions: ['validate-blobs'],
|
|
1357
|
+
sender: this.getSenderAddress().toString(),
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1106
1360
|
throw new Error('Failed to validate blobs');
|
|
1107
1361
|
});
|
|
1108
1362
|
}
|
|
@@ -1113,8 +1367,7 @@ export class SequencerPublisher {
|
|
|
1113
1367
|
header: encodedData.header.toViem(),
|
|
1114
1368
|
archive: toHex(encodedData.archive),
|
|
1115
1369
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1370
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1371
|
},
|
|
1119
1372
|
},
|
|
1120
1373
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1140,7 +1393,7 @@ export class SequencerPublisher {
|
|
|
1140
1393
|
readonly header: ViemHeader;
|
|
1141
1394
|
readonly archive: `0x${string}`;
|
|
1142
1395
|
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier:
|
|
1396
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1144
1397
|
};
|
|
1145
1398
|
},
|
|
1146
1399
|
ViemCommitteeAttestations,
|
|
@@ -1182,6 +1435,8 @@ export class SequencerPublisher {
|
|
|
1182
1435
|
});
|
|
1183
1436
|
}
|
|
1184
1437
|
|
|
1438
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1439
|
+
|
|
1185
1440
|
const simulationResult = await this.l1TxUtils
|
|
1186
1441
|
.simulate(
|
|
1187
1442
|
{
|
|
@@ -1215,6 +1470,18 @@ export class SequencerPublisher {
|
|
|
1215
1470
|
};
|
|
1216
1471
|
}
|
|
1217
1472
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1473
|
+
this.backupFailedTx({
|
|
1474
|
+
id: keccak256(rollupData),
|
|
1475
|
+
failureType: 'simulation',
|
|
1476
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1477
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1478
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1479
|
+
context: {
|
|
1480
|
+
actions: ['propose'],
|
|
1481
|
+
slot: Number(args[0].header.slotNumber),
|
|
1482
|
+
sender: this.getSenderAddress().toString(),
|
|
1483
|
+
},
|
|
1484
|
+
});
|
|
1218
1485
|
throw err;
|
|
1219
1486
|
});
|
|
1220
1487
|
|