@aztec/sequencer-client 3.0.0-nightly.20251111 → 3.0.0-nightly.20251113
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.map +1 -1
- package/dest/config.js +5 -0
- package/dest/publisher/config.d.ts +2 -0
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +5 -0
- package/dest/publisher/sequencer-publisher.d.ts +11 -0
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +92 -41
- package/dest/sequencer/metrics.d.ts +8 -0
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +38 -0
- package/dest/sequencer/sequencer.d.ts +2 -0
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +114 -24
- package/package.json +27 -27
- package/src/config.ts +6 -0
- package/src/publisher/config.ts +8 -0
- package/src/publisher/sequencer-publisher.ts +101 -41
- package/src/sequencer/metrics.ts +48 -0
- package/src/sequencer/sequencer.ts +120 -23
|
@@ -69,6 +69,7 @@ export { SequencerState };
|
|
|
69
69
|
lastBlockPublished;
|
|
70
70
|
governanceProposerPayload;
|
|
71
71
|
/** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
|
|
72
|
+
/** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */ lastSlotForValidationBlock;
|
|
72
73
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
73
74
|
enforceTimeTable;
|
|
74
75
|
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
@@ -79,6 +80,10 @@ export { SequencerState };
|
|
|
79
80
|
publisher;
|
|
80
81
|
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
81
82
|
super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.blockBuilder = blockBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.config = config, this.telemetry = telemetry, this.log = log, this.pollingIntervalMs = 1000, this.maxTxsPerBlock = 32, this.minTxsPerBlock = 1, this.maxL1TxInclusionTimeIntoSlot = 0, this.state = SequencerState.STOPPED, this.maxBlockSizeInBytes = 1024 * 1024, this.maxBlockGas = new Gas(100e9, 100e9), this.enforceTimeTable = false;
|
|
83
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
84
|
+
if (this.config.fishermanMode) {
|
|
85
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
86
|
+
}
|
|
82
87
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
83
88
|
// Initialize config
|
|
84
89
|
this.updateConfig(this.config);
|
|
@@ -215,25 +220,50 @@ export { SequencerState };
|
|
|
215
220
|
// Check that we are a proposer for the next slot
|
|
216
221
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
217
222
|
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
218
|
-
// If we are not a proposer
|
|
223
|
+
// If we are not a proposer check if we should invalidate a invalid block, and bail
|
|
219
224
|
if (!canPropose) {
|
|
220
225
|
await this.considerInvalidatingBlock(syncedTo, slot);
|
|
221
226
|
return;
|
|
222
227
|
}
|
|
228
|
+
// In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
|
|
229
|
+
if (this.config.fishermanMode) {
|
|
230
|
+
if (this.lastSlotForValidationBlock === slot) {
|
|
231
|
+
this.log.trace(`Already validated block building for slot ${slot} (skipping)`, {
|
|
232
|
+
slot
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.log.debug(`Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`, {
|
|
237
|
+
slot,
|
|
238
|
+
proposer: proposer?.toString()
|
|
239
|
+
});
|
|
240
|
+
// Mark this slot as being validated
|
|
241
|
+
this.lastSlotForValidationBlock = slot;
|
|
242
|
+
}
|
|
223
243
|
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
224
244
|
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
225
245
|
this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
226
246
|
...syncLogData,
|
|
227
247
|
block: syncedTo.block.header.toInspect()
|
|
228
248
|
});
|
|
249
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
|
|
229
250
|
return;
|
|
230
251
|
}
|
|
231
252
|
// We now need to get ourselves a publisher.
|
|
232
253
|
// The returned attestor will be the one we provided if we provided one.
|
|
233
254
|
// Otherwise it will be a valid attestor for the returned publisher.
|
|
234
|
-
|
|
255
|
+
// In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
|
|
256
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(this.config.fishermanMode ? undefined : proposer);
|
|
235
257
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
236
258
|
this.publisher = publisher;
|
|
259
|
+
// In fisherman mode, set the actual proposer's address for simulations
|
|
260
|
+
if (this.config.fishermanMode) {
|
|
261
|
+
if (proposer) {
|
|
262
|
+
publisher.setProposerAddressForSimulation(proposer);
|
|
263
|
+
this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Get proposer credentials
|
|
237
267
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
|
|
238
268
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
|
|
239
269
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
@@ -246,6 +276,7 @@ export { SequencerState };
|
|
|
246
276
|
this.emit('proposer-rollup-check-failed', {
|
|
247
277
|
reason: 'Rollup contract check failed'
|
|
248
278
|
});
|
|
279
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
249
280
|
return;
|
|
250
281
|
} else if (canProposeCheck.slot !== slot) {
|
|
251
282
|
this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${slot} but got ${canProposeCheck.slot}.`, {
|
|
@@ -257,6 +288,7 @@ export { SequencerState };
|
|
|
257
288
|
this.emit('proposer-rollup-check-failed', {
|
|
258
289
|
reason: 'Slot mismatch'
|
|
259
290
|
});
|
|
291
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
260
292
|
return;
|
|
261
293
|
} else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
|
|
262
294
|
this.log.warn(`Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.blockNumber}.`, {
|
|
@@ -268,6 +300,7 @@ export { SequencerState };
|
|
|
268
300
|
this.emit('proposer-rollup-check-failed', {
|
|
269
301
|
reason: 'Block mismatch'
|
|
270
302
|
});
|
|
303
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
271
304
|
return;
|
|
272
305
|
}
|
|
273
306
|
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
|
|
@@ -275,6 +308,7 @@ export { SequencerState };
|
|
|
275
308
|
});
|
|
276
309
|
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
|
|
277
310
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
311
|
+
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
278
312
|
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
|
|
279
313
|
// Enqueues block invalidation
|
|
280
314
|
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
@@ -285,18 +319,41 @@ export { SequencerState };
|
|
|
285
319
|
const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
|
|
286
320
|
// Wait until the voting promises have resolved, so all requests are enqueued
|
|
287
321
|
await Promise.all(votesPromises);
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
322
|
+
// In fisherman mode, we don't publish to L1
|
|
323
|
+
if (this.config.fishermanMode) {
|
|
324
|
+
// Clear pending requests
|
|
325
|
+
publisher.clearPendingRequests();
|
|
326
|
+
if (block) {
|
|
327
|
+
this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
|
|
328
|
+
blockNumber: newBlockNumber,
|
|
329
|
+
slot: Number(slot),
|
|
330
|
+
archive: block.archive.toString(),
|
|
331
|
+
txCount: block.body.txEffects.length
|
|
332
|
+
});
|
|
333
|
+
this.lastBlockPublished = block;
|
|
334
|
+
this.metrics.recordBlockProposalSuccess();
|
|
335
|
+
} else {
|
|
336
|
+
// Block building failed in fisherman mode
|
|
337
|
+
this.log.warn(`Validation block building FAILED for slot ${slot}`, {
|
|
338
|
+
blockNumber: newBlockNumber,
|
|
339
|
+
slot: Number(slot)
|
|
340
|
+
});
|
|
341
|
+
this.metrics.recordBlockProposalFailed('block_build_failed');
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
// Normal mode: send the tx to L1
|
|
345
|
+
const l1Response = await publisher.sendRequests();
|
|
346
|
+
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
347
|
+
if (proposedBlock) {
|
|
348
|
+
this.lastBlockPublished = block;
|
|
349
|
+
this.emit('block-published', {
|
|
350
|
+
blockNumber: newBlockNumber,
|
|
351
|
+
slot: Number(slot)
|
|
352
|
+
});
|
|
353
|
+
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
354
|
+
} else if (block) {
|
|
355
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
356
|
+
}
|
|
300
357
|
}
|
|
301
358
|
this.setState(SequencerState.IDLE, undefined);
|
|
302
359
|
}
|
|
@@ -336,6 +393,7 @@ export { SequencerState };
|
|
|
336
393
|
slot
|
|
337
394
|
});
|
|
338
395
|
}
|
|
396
|
+
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
339
397
|
}
|
|
340
398
|
} else {
|
|
341
399
|
this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
|
|
@@ -347,6 +405,7 @@ export { SequencerState };
|
|
|
347
405
|
minTxs: this.minTxsPerBlock,
|
|
348
406
|
availableTxs: pendingTxCount
|
|
349
407
|
});
|
|
408
|
+
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
350
409
|
}
|
|
351
410
|
return block;
|
|
352
411
|
}
|
|
@@ -476,14 +535,22 @@ export { SequencerState };
|
|
|
476
535
|
txHashes,
|
|
477
536
|
...blockStats
|
|
478
537
|
});
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
this.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
538
|
+
// In fisherman mode, skip attestation collection
|
|
539
|
+
let attestationsAndSigners;
|
|
540
|
+
if (this.config.fishermanMode) {
|
|
541
|
+
this.log.debug('Skipping attestation collection');
|
|
542
|
+
attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
|
|
543
|
+
} else {
|
|
544
|
+
this.log.debug('Collecting attestations');
|
|
545
|
+
attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
|
|
546
|
+
this.log.verbose(`Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`, {
|
|
547
|
+
blockHash,
|
|
548
|
+
blockNumber,
|
|
549
|
+
slot
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
// In fisherman mode, skip attestation signing
|
|
553
|
+
const attestationsAndSignersSignature = this.config.fishermanMode || !this.validatorClient ? Signature.empty() : await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress());
|
|
487
554
|
await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
|
|
488
555
|
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
489
556
|
return block;
|
|
@@ -686,7 +753,18 @@ export { SequencerState };
|
|
|
686
753
|
});
|
|
687
754
|
return false;
|
|
688
755
|
}) : undefined;
|
|
689
|
-
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>
|
|
756
|
+
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>{
|
|
757
|
+
// Record metrics for fisherman mode
|
|
758
|
+
if (this.config.fishermanMode && actions.length > 0) {
|
|
759
|
+
this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
|
|
760
|
+
slot,
|
|
761
|
+
actionCount: actions.length
|
|
762
|
+
});
|
|
763
|
+
this.metrics.recordSlashingAttempt(actions.length);
|
|
764
|
+
}
|
|
765
|
+
// Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
|
|
766
|
+
return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
|
|
767
|
+
}).catch((err)=>{
|
|
690
768
|
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
691
769
|
slot
|
|
692
770
|
});
|
|
@@ -732,6 +810,13 @@ export { SequencerState };
|
|
|
732
810
|
undefined
|
|
733
811
|
];
|
|
734
812
|
}
|
|
813
|
+
// In fisherman mode, just return the current proposer
|
|
814
|
+
if (this.config.fishermanMode) {
|
|
815
|
+
return [
|
|
816
|
+
true,
|
|
817
|
+
proposer
|
|
818
|
+
];
|
|
819
|
+
}
|
|
735
820
|
const validatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
736
821
|
const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
|
|
737
822
|
if (!weAreProposer) {
|
|
@@ -843,7 +928,12 @@ export { SequencerState };
|
|
|
843
928
|
}
|
|
844
929
|
this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
|
|
845
930
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
846
|
-
|
|
931
|
+
if (!this.config.fishermanMode) {
|
|
932
|
+
await publisher.sendRequests();
|
|
933
|
+
} else {
|
|
934
|
+
this.log.info('Invalidating block in fisherman mode, clearing pending requests');
|
|
935
|
+
publisher.clearPendingRequests();
|
|
936
|
+
}
|
|
847
937
|
}
|
|
848
938
|
getSlotStartBuildTimestamp(slotNumber) {
|
|
849
939
|
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/sequencer-client",
|
|
3
|
-
"version": "3.0.0-nightly.
|
|
3
|
+
"version": "3.0.0-nightly.20251113",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -26,37 +26,37 @@
|
|
|
26
26
|
"test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --config jest.integration.config.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@aztec/aztec.js": "3.0.0-nightly.
|
|
30
|
-
"@aztec/bb-prover": "3.0.0-nightly.
|
|
31
|
-
"@aztec/blob-lib": "3.0.0-nightly.
|
|
32
|
-
"@aztec/blob-sink": "3.0.0-nightly.
|
|
33
|
-
"@aztec/constants": "3.0.0-nightly.
|
|
34
|
-
"@aztec/epoch-cache": "3.0.0-nightly.
|
|
35
|
-
"@aztec/ethereum": "3.0.0-nightly.
|
|
36
|
-
"@aztec/foundation": "3.0.0-nightly.
|
|
37
|
-
"@aztec/l1-artifacts": "3.0.0-nightly.
|
|
38
|
-
"@aztec/merkle-tree": "3.0.0-nightly.
|
|
39
|
-
"@aztec/node-keystore": "3.0.0-nightly.
|
|
40
|
-
"@aztec/noir-acvm_js": "3.0.0-nightly.
|
|
41
|
-
"@aztec/noir-contracts.js": "3.0.0-nightly.
|
|
42
|
-
"@aztec/noir-protocol-circuits-types": "3.0.0-nightly.
|
|
43
|
-
"@aztec/noir-types": "3.0.0-nightly.
|
|
44
|
-
"@aztec/p2p": "3.0.0-nightly.
|
|
45
|
-
"@aztec/protocol-contracts": "3.0.0-nightly.
|
|
46
|
-
"@aztec/prover-client": "3.0.0-nightly.
|
|
47
|
-
"@aztec/simulator": "3.0.0-nightly.
|
|
48
|
-
"@aztec/slasher": "3.0.0-nightly.
|
|
49
|
-
"@aztec/stdlib": "3.0.0-nightly.
|
|
50
|
-
"@aztec/telemetry-client": "3.0.0-nightly.
|
|
51
|
-
"@aztec/validator-client": "3.0.0-nightly.
|
|
52
|
-
"@aztec/world-state": "3.0.0-nightly.
|
|
29
|
+
"@aztec/aztec.js": "3.0.0-nightly.20251113",
|
|
30
|
+
"@aztec/bb-prover": "3.0.0-nightly.20251113",
|
|
31
|
+
"@aztec/blob-lib": "3.0.0-nightly.20251113",
|
|
32
|
+
"@aztec/blob-sink": "3.0.0-nightly.20251113",
|
|
33
|
+
"@aztec/constants": "3.0.0-nightly.20251113",
|
|
34
|
+
"@aztec/epoch-cache": "3.0.0-nightly.20251113",
|
|
35
|
+
"@aztec/ethereum": "3.0.0-nightly.20251113",
|
|
36
|
+
"@aztec/foundation": "3.0.0-nightly.20251113",
|
|
37
|
+
"@aztec/l1-artifacts": "3.0.0-nightly.20251113",
|
|
38
|
+
"@aztec/merkle-tree": "3.0.0-nightly.20251113",
|
|
39
|
+
"@aztec/node-keystore": "3.0.0-nightly.20251113",
|
|
40
|
+
"@aztec/noir-acvm_js": "3.0.0-nightly.20251113",
|
|
41
|
+
"@aztec/noir-contracts.js": "3.0.0-nightly.20251113",
|
|
42
|
+
"@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251113",
|
|
43
|
+
"@aztec/noir-types": "3.0.0-nightly.20251113",
|
|
44
|
+
"@aztec/p2p": "3.0.0-nightly.20251113",
|
|
45
|
+
"@aztec/protocol-contracts": "3.0.0-nightly.20251113",
|
|
46
|
+
"@aztec/prover-client": "3.0.0-nightly.20251113",
|
|
47
|
+
"@aztec/simulator": "3.0.0-nightly.20251113",
|
|
48
|
+
"@aztec/slasher": "3.0.0-nightly.20251113",
|
|
49
|
+
"@aztec/stdlib": "3.0.0-nightly.20251113",
|
|
50
|
+
"@aztec/telemetry-client": "3.0.0-nightly.20251113",
|
|
51
|
+
"@aztec/validator-client": "3.0.0-nightly.20251113",
|
|
52
|
+
"@aztec/world-state": "3.0.0-nightly.20251113",
|
|
53
53
|
"lodash.chunk": "^4.2.0",
|
|
54
54
|
"tslib": "^2.4.0",
|
|
55
55
|
"viem": "npm:@spalladino/viem@2.38.2-eip7594.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@aztec/archiver": "3.0.0-nightly.
|
|
59
|
-
"@aztec/kv-store": "3.0.0-nightly.
|
|
58
|
+
"@aztec/archiver": "3.0.0-nightly.20251113",
|
|
59
|
+
"@aztec/kv-store": "3.0.0-nightly.20251113",
|
|
60
60
|
"@jest/globals": "^30.0.0",
|
|
61
61
|
"@types/jest": "^30.0.0",
|
|
62
62
|
"@types/lodash.chunk": "^4.2.7",
|
package/src/config.ts
CHANGED
|
@@ -153,6 +153,12 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
|
|
|
153
153
|
description: 'Inject a fake attestation (for testing only)',
|
|
154
154
|
...booleanConfigHelper(false),
|
|
155
155
|
},
|
|
156
|
+
fishermanMode: {
|
|
157
|
+
env: 'FISHERMAN_MODE',
|
|
158
|
+
description:
|
|
159
|
+
'Whether to run in fisherman mode: builds blocks on every slot for validation without publishing to L1',
|
|
160
|
+
...booleanConfigHelper(false),
|
|
161
|
+
},
|
|
156
162
|
shuffleAttestationOrdering: {
|
|
157
163
|
description: 'Shuffle attestation ordering to create invalid ordering (for testing only)',
|
|
158
164
|
...booleanConfigHelper(false),
|
package/src/publisher/config.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type PublisherConfig = L1TxUtilsConfig &
|
|
|
35
35
|
BlobSinkConfig & {
|
|
36
36
|
/** True to use publishers in invalid states (timed out, cancelled, etc) if no other is available */
|
|
37
37
|
publisherAllowInvalidStates?: boolean;
|
|
38
|
+
/** Whether to run in fisherman mode: builds blocks on every slot for validation without publishing to L1 */
|
|
39
|
+
fishermanMode?: boolean;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
export const getTxSenderConfigMappings: (
|
|
@@ -68,6 +70,12 @@ export const getPublisherConfigMappings: (
|
|
|
68
70
|
env: scope === `PROVER` ? `PROVER_PUBLISHER_ALLOW_INVALID_STATES` : `SEQ_PUBLISHER_ALLOW_INVALID_STATES`,
|
|
69
71
|
...booleanConfigHelper(true),
|
|
70
72
|
},
|
|
73
|
+
fishermanMode: {
|
|
74
|
+
env: 'FISHERMAN_MODE',
|
|
75
|
+
description:
|
|
76
|
+
'Whether to run in fisherman mode: builds blocks on every slot for validation without publishing to L1',
|
|
77
|
+
...booleanConfigHelper(false),
|
|
78
|
+
},
|
|
71
79
|
...l1TxUtilsConfigMappings,
|
|
72
80
|
...blobSinkConfigMapping,
|
|
73
81
|
});
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type ViemCommitteeAttestations,
|
|
20
20
|
type ViemHeader,
|
|
21
21
|
type ViemStateReference,
|
|
22
|
+
WEI_CONST,
|
|
22
23
|
formatViemError,
|
|
23
24
|
tryExtractEvent,
|
|
24
25
|
} from '@aztec/ethereum';
|
|
@@ -40,7 +41,7 @@ import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
|
|
|
40
41
|
import { StateReference } from '@aztec/stdlib/tx';
|
|
41
42
|
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
42
43
|
|
|
43
|
-
import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
44
|
+
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
44
45
|
|
|
45
46
|
import type { PublisherConfig, TxSenderConfig } from './config.js';
|
|
46
47
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
@@ -114,6 +115,9 @@ export class SequencerPublisher {
|
|
|
114
115
|
protected ethereumSlotDuration: bigint;
|
|
115
116
|
|
|
116
117
|
private blobSinkClient: BlobSinkClientInterface;
|
|
118
|
+
|
|
119
|
+
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
120
|
+
private proposerAddressForSimulation?: EthAddress;
|
|
117
121
|
// @note - with blobs, the below estimate seems too large.
|
|
118
122
|
// Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
|
|
119
123
|
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
@@ -183,6 +187,14 @@ export class SequencerPublisher {
|
|
|
183
187
|
return this.l1TxUtils.getSenderAddress();
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Sets the proposer address to use for simulations in fisherman mode.
|
|
192
|
+
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
193
|
+
*/
|
|
194
|
+
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
195
|
+
this.proposerAddressForSimulation = proposerAddress;
|
|
196
|
+
}
|
|
197
|
+
|
|
186
198
|
public addRequest(request: RequestWithExpiry) {
|
|
187
199
|
this.requests.push(request);
|
|
188
200
|
}
|
|
@@ -191,6 +203,17 @@ export class SequencerPublisher {
|
|
|
191
203
|
return this.epochCache.getEpochAndSlotNow().slot;
|
|
192
204
|
}
|
|
193
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Clears all pending requests without sending them.
|
|
208
|
+
*/
|
|
209
|
+
public clearPendingRequests(): void {
|
|
210
|
+
const count = this.requests.length;
|
|
211
|
+
this.requests = [];
|
|
212
|
+
if (count > 0) {
|
|
213
|
+
this.log.debug(`Cleared ${count} pending request(s)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
194
217
|
/**
|
|
195
218
|
* Sends all requests that are still valid.
|
|
196
219
|
* @returns one of:
|
|
@@ -353,10 +376,20 @@ export class SequencerPublisher {
|
|
|
353
376
|
] as const;
|
|
354
377
|
|
|
355
378
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
379
|
+
const stateOverrides = await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber);
|
|
380
|
+
let balance = 0n;
|
|
381
|
+
if (this.config.fishermanMode) {
|
|
382
|
+
// In fisherman mode, we can't know where the proposer is publishing from
|
|
383
|
+
// so we just add sufficient balance to the multicall3 address
|
|
384
|
+
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
385
|
+
} else {
|
|
386
|
+
balance = await this.l1TxUtils.getSenderBalance();
|
|
387
|
+
}
|
|
388
|
+
stateOverrides.push({
|
|
389
|
+
address: MULTI_CALL_3_ADDRESS,
|
|
390
|
+
balance,
|
|
391
|
+
});
|
|
356
392
|
|
|
357
|
-
// use sender balance to simulate
|
|
358
|
-
const balance = await this.l1TxUtils.getSenderBalance();
|
|
359
|
-
this.log.debug(`Simulating validateHeader with balance: ${balance}`);
|
|
360
393
|
await this.l1TxUtils.simulate(
|
|
361
394
|
{
|
|
362
395
|
to: this.rollupContract.address,
|
|
@@ -364,10 +397,7 @@ export class SequencerPublisher {
|
|
|
364
397
|
from: MULTI_CALL_3_ADDRESS,
|
|
365
398
|
},
|
|
366
399
|
{ time: ts + 1n },
|
|
367
|
-
|
|
368
|
-
{ address: MULTI_CALL_3_ADDRESS, balance },
|
|
369
|
-
...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
|
|
370
|
-
],
|
|
400
|
+
stateOverrides,
|
|
371
401
|
);
|
|
372
402
|
this.log.debug(`Simulated validateHeader`);
|
|
373
403
|
}
|
|
@@ -914,29 +944,39 @@ export class SequencerPublisher {
|
|
|
914
944
|
const kzg = Blob.getViemKzgInstance();
|
|
915
945
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
916
946
|
this.log.debug('Validating blob input', { blobInput });
|
|
917
|
-
const blobEvaluationGas = await this.l1TxUtils
|
|
918
|
-
.estimateGas(
|
|
919
|
-
this.getSenderAddress().toString(),
|
|
920
|
-
{
|
|
921
|
-
to: this.rollupContract.address,
|
|
922
|
-
data: encodeFunctionData({
|
|
923
|
-
abi: RollupAbi,
|
|
924
|
-
functionName: 'validateBlobs',
|
|
925
|
-
args: [blobInput],
|
|
926
|
-
}),
|
|
927
|
-
},
|
|
928
|
-
{},
|
|
929
|
-
{
|
|
930
|
-
blobs: encodedData.blobs.map(b => b.data),
|
|
931
|
-
kzg,
|
|
932
|
-
},
|
|
933
|
-
)
|
|
934
|
-
.catch(err => {
|
|
935
|
-
const { message, metaMessages } = formatViemError(err);
|
|
936
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
937
|
-
throw new Error('Failed to validate blobs');
|
|
938
|
-
});
|
|
939
947
|
|
|
948
|
+
// Get blob evaluation gas
|
|
949
|
+
let blobEvaluationGas: bigint;
|
|
950
|
+
if (this.config.fishermanMode) {
|
|
951
|
+
// In fisherman mode, we can't estimate blob gas because estimateGas doesn't support state overrides
|
|
952
|
+
// Use a fixed estimate.
|
|
953
|
+
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
954
|
+
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
955
|
+
} else {
|
|
956
|
+
// Normal mode - use estimateGas with blob inputs
|
|
957
|
+
blobEvaluationGas = await this.l1TxUtils
|
|
958
|
+
.estimateGas(
|
|
959
|
+
this.getSenderAddress().toString(),
|
|
960
|
+
{
|
|
961
|
+
to: this.rollupContract.address,
|
|
962
|
+
data: encodeFunctionData({
|
|
963
|
+
abi: RollupAbi,
|
|
964
|
+
functionName: 'validateBlobs',
|
|
965
|
+
args: [blobInput],
|
|
966
|
+
}),
|
|
967
|
+
},
|
|
968
|
+
{},
|
|
969
|
+
{
|
|
970
|
+
blobs: encodedData.blobs.map(b => b.data),
|
|
971
|
+
kzg,
|
|
972
|
+
},
|
|
973
|
+
)
|
|
974
|
+
.catch(err => {
|
|
975
|
+
const { message, metaMessages } = formatViemError(err);
|
|
976
|
+
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
977
|
+
throw new Error('Failed to validate blobs');
|
|
978
|
+
});
|
|
979
|
+
}
|
|
940
980
|
const signers = encodedData.attestationsAndSigners.getSigners().map(signer => signer.toString());
|
|
941
981
|
|
|
942
982
|
const args = [
|
|
@@ -997,12 +1037,31 @@ export class SequencerPublisher {
|
|
|
997
1037
|
: []
|
|
998
1038
|
).flatMap(override => override.stateDiff ?? []);
|
|
999
1039
|
|
|
1040
|
+
const stateOverrides: StateOverride = [
|
|
1041
|
+
{
|
|
1042
|
+
address: this.rollupContract.address,
|
|
1043
|
+
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1044
|
+
stateDiff: [
|
|
1045
|
+
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1046
|
+
...forcePendingBlockNumberStateDiff,
|
|
1047
|
+
],
|
|
1048
|
+
},
|
|
1049
|
+
];
|
|
1050
|
+
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1051
|
+
if (this.proposerAddressForSimulation) {
|
|
1052
|
+
stateOverrides.push({
|
|
1053
|
+
address: this.proposerAddressForSimulation.toString(),
|
|
1054
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1000
1058
|
const simulationResult = await this.l1TxUtils
|
|
1001
1059
|
.simulate(
|
|
1002
1060
|
{
|
|
1003
1061
|
to: this.rollupContract.address,
|
|
1004
1062
|
data: rollupData,
|
|
1005
1063
|
gas: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
1064
|
+
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1006
1065
|
},
|
|
1007
1066
|
{
|
|
1008
1067
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
@@ -1010,16 +1069,7 @@ export class SequencerPublisher {
|
|
|
1010
1069
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1011
1070
|
gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
|
|
1012
1071
|
},
|
|
1013
|
-
|
|
1014
|
-
{
|
|
1015
|
-
address: this.rollupContract.address,
|
|
1016
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1017
|
-
stateDiff: [
|
|
1018
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1019
|
-
...forcePendingBlockNumberStateDiff,
|
|
1020
|
-
],
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1072
|
+
stateOverrides,
|
|
1023
1073
|
RollupAbi,
|
|
1024
1074
|
{
|
|
1025
1075
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
@@ -1027,7 +1077,17 @@ export class SequencerPublisher {
|
|
|
1027
1077
|
},
|
|
1028
1078
|
)
|
|
1029
1079
|
.catch(err => {
|
|
1030
|
-
|
|
1080
|
+
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1081
|
+
const viemError = formatViemError(err);
|
|
1082
|
+
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1083
|
+
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1084
|
+
// Return a minimal simulation result with the fallback gas estimate
|
|
1085
|
+
return {
|
|
1086
|
+
gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
1087
|
+
logs: [],
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1031
1091
|
throw err;
|
|
1032
1092
|
});
|
|
1033
1093
|
|
package/src/sequencer/metrics.ts
CHANGED
|
@@ -36,6 +36,11 @@ export class SequencerMetrics {
|
|
|
36
36
|
private slots: UpDownCounter;
|
|
37
37
|
private filledSlots: UpDownCounter;
|
|
38
38
|
|
|
39
|
+
private blockProposalFailed: UpDownCounter;
|
|
40
|
+
private blockProposalSuccess: UpDownCounter;
|
|
41
|
+
private blockProposalPrecheckFailed: UpDownCounter;
|
|
42
|
+
private slashingAttempts: UpDownCounter;
|
|
43
|
+
|
|
39
44
|
private lastSeenSlot?: bigint;
|
|
40
45
|
|
|
41
46
|
constructor(
|
|
@@ -121,6 +126,29 @@ export class SequencerMetrics {
|
|
|
121
126
|
valueType: ValueType.INT,
|
|
122
127
|
description: 'The minimum number of attestations required to publish a block',
|
|
123
128
|
});
|
|
129
|
+
|
|
130
|
+
this.blockProposalFailed = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT, {
|
|
131
|
+
valueType: ValueType.INT,
|
|
132
|
+
description: 'The number of times block proposal failed (including validation builds)',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.blockProposalSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT, {
|
|
136
|
+
valueType: ValueType.INT,
|
|
137
|
+
description: 'The number of times block proposal succeeded (including validation builds)',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.blockProposalPrecheckFailed = this.meter.createUpDownCounter(
|
|
141
|
+
Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
|
|
142
|
+
{
|
|
143
|
+
valueType: ValueType.INT,
|
|
144
|
+
description: 'The number of times block proposal pre-build checks failed',
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
this.slashingAttempts = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT, {
|
|
149
|
+
valueType: ValueType.INT,
|
|
150
|
+
description: 'The number of slashing action attempts',
|
|
151
|
+
});
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
public recordRequiredAttestations(requiredAttestationsCount: number, allowanceMs: number) {
|
|
@@ -188,4 +216,24 @@ export class SequencerMetrics {
|
|
|
188
216
|
}
|
|
189
217
|
}
|
|
190
218
|
}
|
|
219
|
+
|
|
220
|
+
recordBlockProposalFailed(reason?: string) {
|
|
221
|
+
this.blockProposalFailed.add(1, {
|
|
222
|
+
...(reason && { [Attributes.ERROR_TYPE]: reason }),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
recordBlockProposalSuccess() {
|
|
227
|
+
this.blockProposalSuccess.add(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
recordBlockProposalPrecheckFailed(checkType: string) {
|
|
231
|
+
this.blockProposalPrecheckFailed.add(1, {
|
|
232
|
+
[Attributes.ERROR_TYPE]: checkType,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
recordSlashingAttempt(actionCount: number) {
|
|
237
|
+
this.slashingAttempts.add(actionCount);
|
|
238
|
+
}
|
|
191
239
|
}
|