@aztec/sequencer-client 0.0.1-commit.4eabbdb → 0.0.1-commit.5358163d3
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/config.d.ts +2 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +13 -8
- 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 +1 -1
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +40 -15
- package/dest/sequencer/sequencer.d.ts +8 -2
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +6 -1
- package/dest/sequencer/timetable.js +1 -1
- package/package.json +28 -28
- package/src/config.ts +14 -8
- 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 +58 -11
- package/src/sequencer/sequencer.ts +8 -1
- package/src/sequencer/timetable.ts +1 -1
|
@@ -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
|
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
SlotNumber,
|
|
10
10
|
} from '@aztec/foundation/branded-types';
|
|
11
11
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
12
|
+
import {
|
|
13
|
+
flipSignature,
|
|
14
|
+
generateRecoverableSignature,
|
|
15
|
+
generateUnrecoverableSignature,
|
|
16
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
12
17
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
13
18
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
19
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
@@ -38,7 +43,7 @@ import {
|
|
|
38
43
|
} from '@aztec/stdlib/interfaces/server';
|
|
39
44
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
40
45
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
41
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
46
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
47
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
48
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
49
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -743,11 +748,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
743
748
|
|
|
744
749
|
collectedAttestationsCount = attestations.length;
|
|
745
750
|
|
|
751
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
752
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
753
|
+
const trimmed = trimAttestations(
|
|
754
|
+
attestations,
|
|
755
|
+
numberOfRequiredAttestations,
|
|
756
|
+
this.attestorAddress,
|
|
757
|
+
localAddresses,
|
|
758
|
+
);
|
|
759
|
+
if (trimmed.length < attestations.length) {
|
|
760
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
761
|
+
}
|
|
762
|
+
|
|
746
763
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
747
|
-
const sorted = orderAttestations(
|
|
764
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
748
765
|
|
|
749
766
|
// Manipulate the attestations if we've been configured to do so
|
|
750
|
-
if (
|
|
767
|
+
if (
|
|
768
|
+
this.config.injectFakeAttestation ||
|
|
769
|
+
this.config.injectHighSValueAttestation ||
|
|
770
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
771
|
+
this.config.shuffleAttestationOrdering
|
|
772
|
+
) {
|
|
751
773
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
752
774
|
}
|
|
753
775
|
|
|
@@ -776,7 +798,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
776
798
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
777
799
|
);
|
|
778
800
|
|
|
779
|
-
if (
|
|
801
|
+
if (
|
|
802
|
+
this.config.injectFakeAttestation ||
|
|
803
|
+
this.config.injectHighSValueAttestation ||
|
|
804
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
805
|
+
) {
|
|
780
806
|
// Find non-empty attestations that are not from the proposer
|
|
781
807
|
const nonProposerIndices: number[] = [];
|
|
782
808
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -786,8 +812,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
786
812
|
}
|
|
787
813
|
if (nonProposerIndices.length > 0) {
|
|
788
814
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
789
|
-
this.
|
|
790
|
-
|
|
815
|
+
if (this.config.injectHighSValueAttestation) {
|
|
816
|
+
this.log.warn(
|
|
817
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
818
|
+
);
|
|
819
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
820
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
821
|
+
this.log.warn(
|
|
822
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
823
|
+
);
|
|
824
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
825
|
+
} else {
|
|
826
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
827
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
828
|
+
}
|
|
791
829
|
}
|
|
792
830
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
793
831
|
}
|
|
@@ -796,11 +834,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
796
834
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
797
835
|
|
|
798
836
|
const shuffled = [...attestations];
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
837
|
+
|
|
838
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
839
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
840
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
841
|
+
const swappable: number[] = [];
|
|
842
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
843
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
844
|
+
swappable.push(k);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (swappable.length >= 2) {
|
|
848
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
849
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
850
|
+
}
|
|
804
851
|
|
|
805
852
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
806
853
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -110,7 +110,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
110
110
|
/** Updates sequencer config by the defined values and updates the timetable */
|
|
111
111
|
public updateConfig(config: Partial<SequencerConfig>) {
|
|
112
112
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
113
|
-
this.log.info(`Updated sequencer config`, omit(filteredConfig, '
|
|
113
|
+
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
114
114
|
this.config = merge(this.config, filteredConfig);
|
|
115
115
|
this.timetable = new SequencerTimetable(
|
|
116
116
|
{
|
|
@@ -422,6 +422,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
422
422
|
);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Returns the current sequencer state.
|
|
427
|
+
*/
|
|
428
|
+
public getState(): SequencerState {
|
|
429
|
+
return this.state;
|
|
430
|
+
}
|
|
431
|
+
|
|
425
432
|
/**
|
|
426
433
|
* Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
|
|
427
434
|
* @param proposedState - The new state to transition to.
|
|
@@ -132,7 +132,7 @@ export class SequencerTimetable {
|
|
|
132
132
|
const initializeDeadline = this.aztecSlotDuration - minWorkToDo;
|
|
133
133
|
this.initializeDeadline = initializeDeadline;
|
|
134
134
|
|
|
135
|
-
this.log.
|
|
135
|
+
this.log.info(
|
|
136
136
|
`Sequencer timetable initialized with ${this.maxNumberOfBlocks} blocks per slot (${this.enforce ? 'enforced' : 'not enforced'})`,
|
|
137
137
|
{
|
|
138
138
|
ethereumSlotDuration: this.ethereumSlotDuration,
|