@aztec/sequencer-client 0.0.1-commit.d6f2b3f94 → 0.0.1-commit.dbf9cec
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 +12 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +15 -4
- package/dest/config.d.ts +3 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +13 -4
- 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/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 +13 -2
- package/dest/publisher/sequencer-publisher.d.ts +22 -7
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +258 -29
- package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +39 -11
- 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 +15 -7
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +23 -25
- package/dest/sequencer/timetable.js +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 +5 -3
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +6 -4
- 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 +28 -28
- package/src/client/sequencer-client.ts +25 -7
- package/src/config.ts +17 -8
- package/src/global_variable_builder/global_builder.ts +1 -1
- 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 +23 -6
- package/src/publisher/sequencer-publisher.ts +241 -36
- package/src/sequencer/checkpoint_proposal_job.ts +59 -10
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +31 -30
- package/src/sequencer/timetable.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +12 -1
- 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,11 +19,11 @@ 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';
|
|
@@ -32,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
|
32
33
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
34
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
34
35
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
36
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
35
37
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
38
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
39
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
@@ -43,9 +45,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
43
45
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
46
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
45
47
|
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
import {
|
|
49
|
+
type Hex,
|
|
50
|
+
type StateOverride,
|
|
51
|
+
type TransactionReceipt,
|
|
52
|
+
type TypedDataDefinition,
|
|
53
|
+
encodeFunctionData,
|
|
54
|
+
keccak256,
|
|
55
|
+
multicall3Abi,
|
|
56
|
+
toHex,
|
|
57
|
+
} from 'viem';
|
|
58
|
+
|
|
59
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
60
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
49
61
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
62
|
|
|
51
63
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -60,6 +72,8 @@ type L1ProcessArgs = {
|
|
|
60
72
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
73
|
/** Attestations and signers signature */
|
|
62
74
|
attestationsAndSignersSignature: Signature;
|
|
75
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
76
|
+
feeAssetPriceModifier: bigint;
|
|
63
77
|
};
|
|
64
78
|
|
|
65
79
|
export const Actions = [
|
|
@@ -105,6 +119,7 @@ export class SequencerPublisher {
|
|
|
105
119
|
private interrupted = false;
|
|
106
120
|
private metrics: SequencerPublisherMetrics;
|
|
107
121
|
public epochCache: EpochCache;
|
|
122
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
108
123
|
|
|
109
124
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
110
125
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -112,6 +127,7 @@ export class SequencerPublisher {
|
|
|
112
127
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
128
|
|
|
114
129
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
130
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
115
131
|
|
|
116
132
|
protected log: Logger;
|
|
117
133
|
protected ethereumSlotDuration: bigint;
|
|
@@ -123,13 +139,17 @@ export class SequencerPublisher {
|
|
|
123
139
|
|
|
124
140
|
/** L1 fee analyzer for fisherman mode */
|
|
125
141
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
142
|
+
|
|
143
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
144
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
145
|
+
|
|
126
146
|
// A CALL to a cold address is 2700 gas
|
|
127
147
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
148
|
|
|
129
149
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
130
150
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
131
151
|
|
|
132
|
-
public l1TxUtils:
|
|
152
|
+
public l1TxUtils: L1TxUtils;
|
|
133
153
|
public rollupContract: RollupContract;
|
|
134
154
|
public govProposerContract: GovernanceProposerContract;
|
|
135
155
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -140,11 +160,12 @@ export class SequencerPublisher {
|
|
|
140
160
|
protected requests: RequestWithExpiry[] = [];
|
|
141
161
|
|
|
142
162
|
constructor(
|
|
143
|
-
private config:
|
|
163
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
164
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
144
165
|
deps: {
|
|
145
166
|
telemetry?: TelemetryClient;
|
|
146
167
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
168
|
+
l1TxUtils: L1TxUtils;
|
|
148
169
|
rollupContract: RollupContract;
|
|
149
170
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
150
171
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -188,12 +209,52 @@ export class SequencerPublisher {
|
|
|
188
209
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
210
|
);
|
|
190
211
|
}
|
|
212
|
+
|
|
213
|
+
// Initialize fee asset price oracle
|
|
214
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
215
|
+
this.l1TxUtils.client,
|
|
216
|
+
this.rollupContract,
|
|
217
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
221
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
226
|
+
* Does nothing if no store is configured.
|
|
227
|
+
*/
|
|
228
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
229
|
+
if (!this.failedTxStore) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tx: FailedL1Tx = {
|
|
234
|
+
...failedTx,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Fire and forget - don't block on backup
|
|
239
|
+
void this.failedTxStore
|
|
240
|
+
.then(store => store?.saveFailedTx(tx))
|
|
241
|
+
.catch(err => {
|
|
242
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
243
|
+
});
|
|
191
244
|
}
|
|
192
245
|
|
|
193
246
|
public getRollupContract(): RollupContract {
|
|
194
247
|
return this.rollupContract;
|
|
195
248
|
}
|
|
196
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Gets the fee asset price modifier from the oracle.
|
|
252
|
+
* Returns 0n if the oracle query fails.
|
|
253
|
+
*/
|
|
254
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
255
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
256
|
+
}
|
|
257
|
+
|
|
197
258
|
public getSenderAddress() {
|
|
198
259
|
return this.l1TxUtils.getSenderAddress();
|
|
199
260
|
}
|
|
@@ -361,6 +422,21 @@ export class SequencerPublisher {
|
|
|
361
422
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
362
423
|
|
|
363
424
|
try {
|
|
425
|
+
// Capture context for failed tx backup before sending
|
|
426
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
427
|
+
const multicallData = encodeFunctionData({
|
|
428
|
+
abi: multicall3Abi,
|
|
429
|
+
functionName: 'aggregate3',
|
|
430
|
+
args: [
|
|
431
|
+
validRequests.map(r => ({
|
|
432
|
+
target: r.request.to!,
|
|
433
|
+
callData: r.request.data!,
|
|
434
|
+
allowFailure: true,
|
|
435
|
+
})),
|
|
436
|
+
],
|
|
437
|
+
});
|
|
438
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
439
|
+
|
|
364
440
|
this.log.debug('Forwarding transactions', {
|
|
365
441
|
validRequests: validRequests.map(request => request.action),
|
|
366
442
|
txConfig,
|
|
@@ -373,7 +449,12 @@ export class SequencerPublisher {
|
|
|
373
449
|
this.rollupContract.address,
|
|
374
450
|
this.log,
|
|
375
451
|
);
|
|
376
|
-
const
|
|
452
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
453
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
454
|
+
validRequests,
|
|
455
|
+
result,
|
|
456
|
+
txContext,
|
|
457
|
+
);
|
|
377
458
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
378
459
|
} catch (err) {
|
|
379
460
|
const viemError = formatViemError(err);
|
|
@@ -393,11 +474,25 @@ export class SequencerPublisher {
|
|
|
393
474
|
|
|
394
475
|
private callbackBundledTransactions(
|
|
395
476
|
requests: RequestWithExpiry[],
|
|
396
|
-
result
|
|
477
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
478
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
397
479
|
) {
|
|
398
480
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
399
481
|
if (result instanceof FormattedViemError) {
|
|
400
482
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
483
|
+
this.backupFailedTx({
|
|
484
|
+
id: keccak256(txContext.multicallData),
|
|
485
|
+
failureType: 'send-error',
|
|
486
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
487
|
+
blobData: txContext.blobData,
|
|
488
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
489
|
+
error: { message: result.message, name: result.name },
|
|
490
|
+
context: {
|
|
491
|
+
actions: requests.map(r => r.action),
|
|
492
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
493
|
+
sender: this.getSenderAddress().toString(),
|
|
494
|
+
},
|
|
495
|
+
});
|
|
401
496
|
return { failedActions: requests.map(r => r.action) };
|
|
402
497
|
} else {
|
|
403
498
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -410,6 +505,30 @@ export class SequencerPublisher {
|
|
|
410
505
|
failedActions.push(request.action);
|
|
411
506
|
}
|
|
412
507
|
}
|
|
508
|
+
// Single backup for the whole reverted tx
|
|
509
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
510
|
+
this.backupFailedTx({
|
|
511
|
+
id: result.receipt.transactionHash,
|
|
512
|
+
failureType: 'revert',
|
|
513
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
514
|
+
blobData: txContext.blobData,
|
|
515
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
516
|
+
receipt: {
|
|
517
|
+
transactionHash: result.receipt.transactionHash,
|
|
518
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
519
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
520
|
+
status: 'reverted',
|
|
521
|
+
},
|
|
522
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
523
|
+
context: {
|
|
524
|
+
actions: failedActions,
|
|
525
|
+
requests: requests
|
|
526
|
+
.filter(r => failedActions.includes(r.action))
|
|
527
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
528
|
+
sender: this.getSenderAddress().toString(),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
413
532
|
return { successfulActions, failedActions };
|
|
414
533
|
}
|
|
415
534
|
}
|
|
@@ -521,6 +640,8 @@ export class SequencerPublisher {
|
|
|
521
640
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
641
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
523
642
|
|
|
643
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
644
|
+
|
|
524
645
|
try {
|
|
525
646
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
647
|
request,
|
|
@@ -572,6 +693,18 @@ export class SequencerPublisher {
|
|
|
572
693
|
|
|
573
694
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
695
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
696
|
+
this.backupFailedTx({
|
|
697
|
+
id: keccak256(request.data!),
|
|
698
|
+
failureType: 'simulation',
|
|
699
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
700
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
701
|
+
error: { message: viemError.message, name: viemError.name },
|
|
702
|
+
context: {
|
|
703
|
+
actions: [`invalidate-${reason}`],
|
|
704
|
+
checkpointNumber,
|
|
705
|
+
sender: this.getSenderAddress().toString(),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
575
708
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
576
709
|
}
|
|
577
710
|
}
|
|
@@ -617,24 +750,8 @@ export class SequencerPublisher {
|
|
|
617
750
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
618
751
|
): Promise<bigint> {
|
|
619
752
|
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
753
|
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
754
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
638
755
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
756
|
|
|
640
757
|
const args = [
|
|
@@ -642,7 +759,7 @@ export class SequencerPublisher {
|
|
|
642
759
|
header: checkpoint.header.toViem(),
|
|
643
760
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
761
|
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier:
|
|
762
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
646
763
|
},
|
|
647
764
|
},
|
|
648
765
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -691,6 +808,32 @@ export class SequencerPublisher {
|
|
|
691
808
|
return false;
|
|
692
809
|
}
|
|
693
810
|
|
|
811
|
+
// Check if payload was already submitted to governance
|
|
812
|
+
const cacheKey = payload.toString();
|
|
813
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
814
|
+
try {
|
|
815
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
816
|
+
const proposed = await retry(
|
|
817
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
818
|
+
'Check if payload was proposed',
|
|
819
|
+
makeBackoff([0, 1, 2]),
|
|
820
|
+
this.log,
|
|
821
|
+
true,
|
|
822
|
+
);
|
|
823
|
+
if (proposed) {
|
|
824
|
+
this.payloadProposedCache.add(cacheKey);
|
|
825
|
+
}
|
|
826
|
+
} catch (err) {
|
|
827
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
833
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
|
|
694
837
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
838
|
this.lastActions[signalType] = slotNumber;
|
|
696
839
|
const action = signalType;
|
|
@@ -709,11 +852,26 @@ export class SequencerPublisher {
|
|
|
709
852
|
lastValidL2Slot: slotNumber,
|
|
710
853
|
});
|
|
711
854
|
|
|
855
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
856
|
+
|
|
712
857
|
try {
|
|
713
858
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
714
859
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
715
860
|
} catch (err) {
|
|
716
|
-
|
|
861
|
+
const viemError = formatViemError(err);
|
|
862
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
863
|
+
this.backupFailedTx({
|
|
864
|
+
id: keccak256(request.data!),
|
|
865
|
+
failureType: 'simulation',
|
|
866
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
867
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
868
|
+
error: { message: viemError.message, name: viemError.name },
|
|
869
|
+
context: {
|
|
870
|
+
actions: [action],
|
|
871
|
+
slot: slotNumber,
|
|
872
|
+
sender: this.getSenderAddress().toString(),
|
|
873
|
+
},
|
|
874
|
+
});
|
|
717
875
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
718
876
|
}
|
|
719
877
|
|
|
@@ -918,14 +1076,15 @@ export class SequencerPublisher {
|
|
|
918
1076
|
const checkpointHeader = checkpoint.header;
|
|
919
1077
|
|
|
920
1078
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1079
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
1080
|
|
|
923
|
-
const proposeTxArgs = {
|
|
1081
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
1082
|
header: checkpointHeader,
|
|
925
1083
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
1084
|
blobs,
|
|
927
1085
|
attestationsAndSigners,
|
|
928
1086
|
attestationsAndSignersSignature,
|
|
1087
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
1088
|
};
|
|
930
1089
|
|
|
931
1090
|
let ts: bigint;
|
|
@@ -1008,6 +1167,8 @@ export class SequencerPublisher {
|
|
|
1008
1167
|
|
|
1009
1168
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1010
1169
|
|
|
1170
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1171
|
+
|
|
1011
1172
|
let gasUsed: bigint;
|
|
1012
1173
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1013
1174
|
try {
|
|
@@ -1017,6 +1178,19 @@ export class SequencerPublisher {
|
|
|
1017
1178
|
const viemError = formatViemError(err, simulateAbi);
|
|
1018
1179
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
1180
|
|
|
1181
|
+
this.backupFailedTx({
|
|
1182
|
+
id: keccak256(request.data!),
|
|
1183
|
+
failureType: 'simulation',
|
|
1184
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1185
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1186
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1187
|
+
context: {
|
|
1188
|
+
actions: [action],
|
|
1189
|
+
slot: slotNumber,
|
|
1190
|
+
sender: this.getSenderAddress().toString(),
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1020
1194
|
return false;
|
|
1021
1195
|
}
|
|
1022
1196
|
|
|
@@ -1100,9 +1274,27 @@ export class SequencerPublisher {
|
|
|
1100
1274
|
kzg,
|
|
1101
1275
|
},
|
|
1102
1276
|
)
|
|
1103
|
-
.catch(err => {
|
|
1104
|
-
const
|
|
1105
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1277
|
+
.catch(async err => {
|
|
1278
|
+
const viemError = formatViemError(err);
|
|
1279
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1280
|
+
const validateBlobsData = encodeFunctionData({
|
|
1281
|
+
abi: RollupAbi,
|
|
1282
|
+
functionName: 'validateBlobs',
|
|
1283
|
+
args: [blobInput],
|
|
1284
|
+
});
|
|
1285
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1286
|
+
this.backupFailedTx({
|
|
1287
|
+
id: keccak256(validateBlobsData),
|
|
1288
|
+
failureType: 'simulation',
|
|
1289
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1290
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1291
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1292
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1293
|
+
context: {
|
|
1294
|
+
actions: ['validate-blobs'],
|
|
1295
|
+
sender: this.getSenderAddress().toString(),
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1106
1298
|
throw new Error('Failed to validate blobs');
|
|
1107
1299
|
});
|
|
1108
1300
|
}
|
|
@@ -1113,8 +1305,7 @@ export class SequencerPublisher {
|
|
|
1113
1305
|
header: encodedData.header.toViem(),
|
|
1114
1306
|
archive: toHex(encodedData.archive),
|
|
1115
1307
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1308
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1309
|
},
|
|
1119
1310
|
},
|
|
1120
1311
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1140,7 +1331,7 @@ export class SequencerPublisher {
|
|
|
1140
1331
|
readonly header: ViemHeader;
|
|
1141
1332
|
readonly archive: `0x${string}`;
|
|
1142
1333
|
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier:
|
|
1334
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1144
1335
|
};
|
|
1145
1336
|
},
|
|
1146
1337
|
ViemCommitteeAttestations,
|
|
@@ -1182,6 +1373,8 @@ export class SequencerPublisher {
|
|
|
1182
1373
|
});
|
|
1183
1374
|
}
|
|
1184
1375
|
|
|
1376
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1377
|
+
|
|
1185
1378
|
const simulationResult = await this.l1TxUtils
|
|
1186
1379
|
.simulate(
|
|
1187
1380
|
{
|
|
@@ -1215,6 +1408,18 @@ export class SequencerPublisher {
|
|
|
1215
1408
|
};
|
|
1216
1409
|
}
|
|
1217
1410
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1411
|
+
this.backupFailedTx({
|
|
1412
|
+
id: keccak256(rollupData),
|
|
1413
|
+
failureType: 'simulation',
|
|
1414
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1415
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1416
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1417
|
+
context: {
|
|
1418
|
+
actions: ['propose'],
|
|
1419
|
+
slot: Number(args[0].header.slotNumber),
|
|
1420
|
+
sender: this.getSenderAddress().toString(),
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1218
1423
|
throw err;
|
|
1219
1424
|
});
|
|
1220
1425
|
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
} from '@aztec/stdlib/interfaces/server';
|
|
39
39
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
40
40
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
41
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
41
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
42
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
43
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
44
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -129,7 +129,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
129
129
|
await Promise.all(votesPromises);
|
|
130
130
|
|
|
131
131
|
if (checkpoint) {
|
|
132
|
-
this.metrics.
|
|
132
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -186,18 +186,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
186
186
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
187
187
|
|
|
188
188
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
189
|
-
const
|
|
190
|
-
c => c.
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
190
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
191
|
+
.map(c => c.checkpointOutHash);
|
|
192
|
+
|
|
193
|
+
// Get the fee asset price modifier from the oracle
|
|
194
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
193
195
|
|
|
194
196
|
// Create a long-lived forked world state for the checkpoint builder
|
|
195
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
197
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
196
198
|
|
|
197
199
|
// Create checkpoint builder for the entire slot
|
|
198
200
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
199
201
|
this.checkpointNumber,
|
|
200
202
|
checkpointGlobalVariables,
|
|
203
|
+
feeAssetPriceModifier,
|
|
201
204
|
l1ToL2Messages,
|
|
202
205
|
previousCheckpointOutHashes,
|
|
203
206
|
fork,
|
|
@@ -217,6 +220,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
217
220
|
|
|
218
221
|
let blocksInCheckpoint: L2Block[] = [];
|
|
219
222
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
223
|
+
const checkpointBuildTimer = new Timer();
|
|
220
224
|
|
|
221
225
|
try {
|
|
222
226
|
// Main loop: build blocks for the checkpoint
|
|
@@ -244,11 +248,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
244
248
|
return undefined;
|
|
245
249
|
}
|
|
246
250
|
|
|
251
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
252
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
253
|
+
this.log.warn(
|
|
254
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
255
|
+
{ slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
256
|
+
);
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
247
260
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
248
261
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
249
262
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
250
263
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
251
264
|
|
|
265
|
+
// Record checkpoint-level build metrics
|
|
266
|
+
this.metrics.recordCheckpointBuild(
|
|
267
|
+
checkpointBuildTimer.ms(),
|
|
268
|
+
blocksInCheckpoint.length,
|
|
269
|
+
checkpoint.getStats().txCount,
|
|
270
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
271
|
+
);
|
|
272
|
+
|
|
252
273
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
253
274
|
if (this.config.fishermanMode) {
|
|
254
275
|
this.log.info(
|
|
@@ -275,6 +296,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
275
296
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
297
|
checkpoint.header,
|
|
277
298
|
checkpoint.archive.root,
|
|
299
|
+
feeAssetPriceModifier,
|
|
278
300
|
lastBlock,
|
|
279
301
|
this.proposer,
|
|
280
302
|
checkpointProposalOptions,
|
|
@@ -313,6 +335,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
313
335
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
314
336
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
315
337
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
338
|
+
|
|
339
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
340
|
+
if (
|
|
341
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
342
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
343
|
+
) {
|
|
344
|
+
const result = Math.max(0, randomInt(100));
|
|
345
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
346
|
+
this.log.warn(
|
|
347
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
348
|
+
);
|
|
349
|
+
return checkpoint;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
316
353
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
317
354
|
txTimeoutAt,
|
|
318
355
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -516,7 +553,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
516
553
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
517
554
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
518
555
|
const pendingTxs = filter(
|
|
519
|
-
this.p2pClient.
|
|
556
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
520
557
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
521
558
|
);
|
|
522
559
|
|
|
@@ -706,8 +743,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
706
743
|
|
|
707
744
|
collectedAttestationsCount = attestations.length;
|
|
708
745
|
|
|
746
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
747
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
748
|
+
const trimmed = trimAttestations(
|
|
749
|
+
attestations,
|
|
750
|
+
numberOfRequiredAttestations,
|
|
751
|
+
this.attestorAddress,
|
|
752
|
+
localAddresses,
|
|
753
|
+
);
|
|
754
|
+
if (trimmed.length < attestations.length) {
|
|
755
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
756
|
+
}
|
|
757
|
+
|
|
709
758
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
710
|
-
const sorted = orderAttestations(
|
|
759
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
711
760
|
|
|
712
761
|
// Manipulate the attestations if we've been configured to do so
|
|
713
762
|
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
@@ -821,7 +870,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
821
870
|
slot: this.slot,
|
|
822
871
|
feeAnalysisId: feeAnalysis?.id,
|
|
823
872
|
});
|
|
824
|
-
this.metrics.
|
|
873
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
825
874
|
}
|
|
826
875
|
|
|
827
876
|
this.publisher.clearPendingRequests();
|