@aztec/sequencer-client 0.0.1-commit.ff7989d6c → 0.0.1-commit.ffe5b04ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +12 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +85 -13
- package/dest/config.d.ts +23 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -16
- package/dest/publisher/config.d.ts +5 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +6 -1
- 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 +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +14 -0
- package/dest/publisher/sequencer-publisher.d.ts +12 -2
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +258 -9
- 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 +68 -37
- package/dest/sequencer/sequencer.d.ts +9 -6
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +1 -1
- package/dest/sequencer/timetable.d.ts +4 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +6 -7
- package/dest/sequencer/types.d.ts +5 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +4 -6
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +39 -30
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +111 -12
- package/src/config.ts +28 -19
- package/src/publisher/config.ts +9 -0
- 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 +15 -0
- package/src/publisher/sequencer-publisher.ts +237 -15
- package/src/sequencer/checkpoint_proposal_job.ts +90 -44
- package/src/sequencer/sequencer.ts +1 -1
- package/src/sequencer/timetable.ts +7 -7
- package/src/sequencer/types.ts +4 -1
- package/src/test/mock_checkpoint_builder.ts +48 -45
|
@@ -81,8 +81,23 @@ export class SequencerPublisherFactory {
|
|
|
81
81
|
const rollup = this.deps.rollupContract;
|
|
82
82
|
const slashingProposerContract = await rollup.getSlashingProposer();
|
|
83
83
|
|
|
84
|
+
const getNextPublisher = async (excludeAddresses: EthAddress[]): Promise<L1TxUtils | undefined> => {
|
|
85
|
+
const exclusionFilter: PublisherFilter<L1TxUtils> = (utils: L1TxUtils) => {
|
|
86
|
+
if (excludeAddresses.some(addr => addr.equals(utils.getSenderAddress()))) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return filter(utils);
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
return await this.deps.publisherManager.getAvailablePublisher(exclusionFilter);
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
84
98
|
const publisher = new SequencerPublisher(this.sequencerConfig, {
|
|
85
99
|
l1TxUtils: l1Publisher,
|
|
100
|
+
getNextPublisher,
|
|
86
101
|
telemetry: this.deps.telemetry,
|
|
87
102
|
blobClient: this.deps.blobClient,
|
|
88
103
|
rollupContract: this.deps.rollupContract,
|
|
@@ -30,6 +30,7 @@ import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
|
30
30
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
31
31
|
import { pick } from '@aztec/foundation/collection';
|
|
32
32
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
33
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
33
34
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
34
35
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
35
36
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
@@ -45,9 +46,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
45
46
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
46
47
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
47
48
|
|
|
48
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
type Hex,
|
|
51
|
+
type StateOverride,
|
|
52
|
+
type TransactionReceipt,
|
|
53
|
+
type TypedDataDefinition,
|
|
54
|
+
encodeFunctionData,
|
|
55
|
+
keccak256,
|
|
56
|
+
multicall3Abi,
|
|
57
|
+
toHex,
|
|
58
|
+
} from 'viem';
|
|
49
59
|
|
|
50
60
|
import type { SequencerPublisherConfig } from './config.js';
|
|
61
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
51
62
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
52
63
|
|
|
53
64
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -109,6 +120,7 @@ export class SequencerPublisher {
|
|
|
109
120
|
private interrupted = false;
|
|
110
121
|
private metrics: SequencerPublisherMetrics;
|
|
111
122
|
public epochCache: EpochCache;
|
|
123
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
112
124
|
|
|
113
125
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
114
126
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -126,6 +138,9 @@ export class SequencerPublisher {
|
|
|
126
138
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
127
139
|
private proposerAddressForSimulation?: EthAddress;
|
|
128
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
|
+
|
|
129
144
|
/** L1 fee analyzer for fisherman mode */
|
|
130
145
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
131
146
|
|
|
@@ -149,7 +164,7 @@ export class SequencerPublisher {
|
|
|
149
164
|
protected requests: RequestWithExpiry[] = [];
|
|
150
165
|
|
|
151
166
|
constructor(
|
|
152
|
-
private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
|
|
167
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
153
168
|
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
154
169
|
deps: {
|
|
155
170
|
telemetry?: TelemetryClient;
|
|
@@ -164,6 +179,7 @@ export class SequencerPublisher {
|
|
|
164
179
|
metrics: SequencerPublisherMetrics;
|
|
165
180
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
166
181
|
log?: Logger;
|
|
182
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
167
183
|
},
|
|
168
184
|
) {
|
|
169
185
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -177,6 +193,7 @@ export class SequencerPublisher {
|
|
|
177
193
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
178
194
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
179
195
|
this.l1TxUtils = deps.l1TxUtils;
|
|
196
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
180
197
|
|
|
181
198
|
this.rollupContract = deps.rollupContract;
|
|
182
199
|
|
|
@@ -205,6 +222,31 @@ export class SequencerPublisher {
|
|
|
205
222
|
this.rollupContract,
|
|
206
223
|
createLogger('sequencer:publisher:price-oracle'),
|
|
207
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
|
+
});
|
|
208
250
|
}
|
|
209
251
|
|
|
210
252
|
public getRollupContract(): RollupContract {
|
|
@@ -386,19 +428,36 @@ export class SequencerPublisher {
|
|
|
386
428
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
387
429
|
|
|
388
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
|
+
|
|
389
448
|
this.log.debug('Forwarding transactions', {
|
|
390
449
|
validRequests: validRequests.map(request => request.action),
|
|
391
450
|
txConfig,
|
|
392
451
|
});
|
|
393
|
-
const result = await
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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,
|
|
400
460
|
);
|
|
401
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
402
461
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
403
462
|
} catch (err) {
|
|
404
463
|
const viemError = formatViemError(err);
|
|
@@ -416,13 +475,76 @@ export class SequencerPublisher {
|
|
|
416
475
|
}
|
|
417
476
|
}
|
|
418
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
|
+
|
|
419
527
|
private callbackBundledTransactions(
|
|
420
528
|
requests: RequestWithExpiry[],
|
|
421
|
-
result
|
|
529
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
530
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
422
531
|
) {
|
|
423
532
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
424
533
|
if (result instanceof FormattedViemError) {
|
|
425
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
|
+
});
|
|
426
548
|
return { failedActions: requests.map(r => r.action) };
|
|
427
549
|
} else {
|
|
428
550
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
@@ -435,6 +557,30 @@ export class SequencerPublisher {
|
|
|
435
557
|
failedActions.push(request.action);
|
|
436
558
|
}
|
|
437
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
|
+
}
|
|
438
584
|
return { successfulActions, failedActions };
|
|
439
585
|
}
|
|
440
586
|
}
|
|
@@ -546,6 +692,8 @@ export class SequencerPublisher {
|
|
|
546
692
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
547
693
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
548
694
|
|
|
695
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
696
|
+
|
|
549
697
|
try {
|
|
550
698
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
551
699
|
request,
|
|
@@ -597,6 +745,18 @@ export class SequencerPublisher {
|
|
|
597
745
|
|
|
598
746
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
599
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
|
+
});
|
|
600
760
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
601
761
|
}
|
|
602
762
|
}
|
|
@@ -744,11 +904,26 @@ export class SequencerPublisher {
|
|
|
744
904
|
lastValidL2Slot: slotNumber,
|
|
745
905
|
});
|
|
746
906
|
|
|
907
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
908
|
+
|
|
747
909
|
try {
|
|
748
910
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
749
911
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
750
912
|
} catch (err) {
|
|
751
|
-
|
|
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
|
+
});
|
|
752
927
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
753
928
|
}
|
|
754
929
|
|
|
@@ -1044,6 +1219,8 @@ export class SequencerPublisher {
|
|
|
1044
1219
|
|
|
1045
1220
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1046
1221
|
|
|
1222
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1223
|
+
|
|
1047
1224
|
let gasUsed: bigint;
|
|
1048
1225
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1049
1226
|
try {
|
|
@@ -1053,6 +1230,19 @@ export class SequencerPublisher {
|
|
|
1053
1230
|
const viemError = formatViemError(err, simulateAbi);
|
|
1054
1231
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1055
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
|
+
|
|
1056
1246
|
return false;
|
|
1057
1247
|
}
|
|
1058
1248
|
|
|
@@ -1136,9 +1326,27 @@ export class SequencerPublisher {
|
|
|
1136
1326
|
kzg,
|
|
1137
1327
|
},
|
|
1138
1328
|
)
|
|
1139
|
-
.catch(err => {
|
|
1140
|
-
const
|
|
1141
|
-
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
|
+
});
|
|
1142
1350
|
throw new Error('Failed to validate blobs');
|
|
1143
1351
|
});
|
|
1144
1352
|
}
|
|
@@ -1217,6 +1425,8 @@ export class SequencerPublisher {
|
|
|
1217
1425
|
});
|
|
1218
1426
|
}
|
|
1219
1427
|
|
|
1428
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1429
|
+
|
|
1220
1430
|
const simulationResult = await this.l1TxUtils
|
|
1221
1431
|
.simulate(
|
|
1222
1432
|
{
|
|
@@ -1250,6 +1460,18 @@ export class SequencerPublisher {
|
|
|
1250
1460
|
};
|
|
1251
1461
|
}
|
|
1252
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
|
+
});
|
|
1253
1475
|
throw err;
|
|
1254
1476
|
});
|
|
1255
1477
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
2
|
import {
|
|
5
3
|
BlockNumber,
|
|
@@ -9,6 +7,11 @@ import {
|
|
|
9
7
|
SlotNumber,
|
|
10
8
|
} from '@aztec/foundation/branded-types';
|
|
11
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
12
15
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
13
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
@@ -27,7 +30,7 @@ import {
|
|
|
27
30
|
type L2BlockSource,
|
|
28
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
29
32
|
} from '@aztec/stdlib/block';
|
|
30
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
31
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
32
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
33
36
|
import {
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
38
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
39
42
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
40
43
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
41
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
44
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
45
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
46
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -262,6 +265,22 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
262
265
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
263
266
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
264
267
|
|
|
268
|
+
// Final validation round for the checkpoint before we propose it, just for safety
|
|
269
|
+
try {
|
|
270
|
+
validateCheckpoint(checkpoint, {
|
|
271
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
272
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
273
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
274
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
275
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
279
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
280
|
+
});
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
265
284
|
// Record checkpoint-level build metrics
|
|
266
285
|
this.metrics.recordCheckpointBuild(
|
|
267
286
|
checkpointBuildTimer.ms(),
|
|
@@ -384,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
384
403
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
385
404
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
386
405
|
|
|
387
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
388
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
389
|
-
|
|
390
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
391
407
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
392
408
|
|
|
@@ -419,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
419
435
|
blockNumber,
|
|
420
436
|
indexWithinCheckpoint,
|
|
421
437
|
txHashesAlreadyIncluded,
|
|
422
|
-
remainingBlobFields,
|
|
423
438
|
});
|
|
424
439
|
|
|
425
440
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -445,12 +460,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
445
460
|
break;
|
|
446
461
|
}
|
|
447
462
|
|
|
448
|
-
const { block, usedTxs
|
|
463
|
+
const { block, usedTxs } = buildResult;
|
|
449
464
|
blocksInCheckpoint.push(block);
|
|
450
465
|
|
|
451
|
-
// Update remaining blob fields for the next block
|
|
452
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
453
|
-
|
|
454
466
|
// Sync the proposed block to the archiver to make it available
|
|
455
467
|
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
456
468
|
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
@@ -518,18 +530,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
518
530
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
519
531
|
buildDeadline: Date | undefined;
|
|
520
532
|
txHashesAlreadyIncluded: Set<string>;
|
|
521
|
-
remainingBlobFields: number;
|
|
522
533
|
},
|
|
523
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
524
|
-
const {
|
|
525
|
-
|
|
526
|
-
forceCreate,
|
|
527
|
-
blockNumber,
|
|
528
|
-
indexWithinCheckpoint,
|
|
529
|
-
buildDeadline,
|
|
530
|
-
txHashesAlreadyIncluded,
|
|
531
|
-
remainingBlobFields,
|
|
532
|
-
} = opts;
|
|
534
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
535
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
536
|
+
opts;
|
|
533
537
|
|
|
534
538
|
this.log.verbose(
|
|
535
539
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -563,16 +567,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
563
567
|
);
|
|
564
568
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
565
569
|
|
|
566
|
-
//
|
|
567
|
-
|
|
568
|
-
const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
|
|
569
|
-
|
|
570
|
+
// Per-block limits derived at startup by computeBlockLimits(), further capped
|
|
571
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
570
572
|
const blockBuilderOptions: PublicProcessorLimits = {
|
|
571
573
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
574
|
+
maxBlockGas:
|
|
575
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
576
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
577
|
+
: undefined,
|
|
575
578
|
deadline: buildDeadline,
|
|
579
|
+
isBuildingProposal: true,
|
|
576
580
|
};
|
|
577
581
|
|
|
578
582
|
// Actually build the block by executing txs
|
|
@@ -602,7 +606,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
602
606
|
}
|
|
603
607
|
|
|
604
608
|
// Block creation succeeded, emit stats and metrics
|
|
605
|
-
const {
|
|
609
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration } = buildResult;
|
|
606
610
|
|
|
607
611
|
const blockStats = {
|
|
608
612
|
eventName: 'l2-block-built',
|
|
@@ -613,7 +617,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
613
617
|
|
|
614
618
|
const blockHash = await block.hash();
|
|
615
619
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
616
|
-
const manaPerSec =
|
|
620
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
617
621
|
|
|
618
622
|
this.log.info(
|
|
619
623
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -621,9 +625,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
621
625
|
);
|
|
622
626
|
|
|
623
627
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
624
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
628
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
625
629
|
|
|
626
|
-
return { block, usedTxs
|
|
630
|
+
return { block, usedTxs };
|
|
627
631
|
} catch (err: any) {
|
|
628
632
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
629
633
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -743,11 +747,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
743
747
|
|
|
744
748
|
collectedAttestationsCount = attestations.length;
|
|
745
749
|
|
|
750
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
751
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
752
|
+
const trimmed = trimAttestations(
|
|
753
|
+
attestations,
|
|
754
|
+
numberOfRequiredAttestations,
|
|
755
|
+
this.attestorAddress,
|
|
756
|
+
localAddresses,
|
|
757
|
+
);
|
|
758
|
+
if (trimmed.length < attestations.length) {
|
|
759
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
760
|
+
}
|
|
761
|
+
|
|
746
762
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
747
|
-
const sorted = orderAttestations(
|
|
763
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
748
764
|
|
|
749
765
|
// Manipulate the attestations if we've been configured to do so
|
|
750
|
-
if (
|
|
766
|
+
if (
|
|
767
|
+
this.config.injectFakeAttestation ||
|
|
768
|
+
this.config.injectHighSValueAttestation ||
|
|
769
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
770
|
+
this.config.shuffleAttestationOrdering
|
|
771
|
+
) {
|
|
751
772
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
752
773
|
}
|
|
753
774
|
|
|
@@ -776,7 +797,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
776
797
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
777
798
|
);
|
|
778
799
|
|
|
779
|
-
if (
|
|
800
|
+
if (
|
|
801
|
+
this.config.injectFakeAttestation ||
|
|
802
|
+
this.config.injectHighSValueAttestation ||
|
|
803
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
804
|
+
) {
|
|
780
805
|
// Find non-empty attestations that are not from the proposer
|
|
781
806
|
const nonProposerIndices: number[] = [];
|
|
782
807
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -786,8 +811,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
786
811
|
}
|
|
787
812
|
if (nonProposerIndices.length > 0) {
|
|
788
813
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
789
|
-
this.
|
|
790
|
-
|
|
814
|
+
if (this.config.injectHighSValueAttestation) {
|
|
815
|
+
this.log.warn(
|
|
816
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
817
|
+
);
|
|
818
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
819
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
820
|
+
this.log.warn(
|
|
821
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
822
|
+
);
|
|
823
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
824
|
+
} else {
|
|
825
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
826
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
827
|
+
}
|
|
791
828
|
}
|
|
792
829
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
793
830
|
}
|
|
@@ -796,11 +833,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
796
833
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
797
834
|
|
|
798
835
|
const shuffled = [...attestations];
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
836
|
+
|
|
837
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
838
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
839
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
840
|
+
const swappable: number[] = [];
|
|
841
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
842
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
843
|
+
swappable.push(k);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (swappable.length >= 2) {
|
|
847
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
848
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
849
|
+
}
|
|
804
850
|
|
|
805
851
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
806
852
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|