@aztec/sequencer-client 2.1.7 → 2.1.8
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 +28 -28
- 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
|
@@ -68,6 +68,7 @@ export { SequencerState };
|
|
|
68
68
|
lastBlockPublished;
|
|
69
69
|
governanceProposerPayload;
|
|
70
70
|
/** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
|
|
71
|
+
/** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */ lastSlotForValidationBlock;
|
|
71
72
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
72
73
|
enforceTimeTable;
|
|
73
74
|
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
@@ -78,6 +79,10 @@ export { SequencerState };
|
|
|
78
79
|
publisher;
|
|
79
80
|
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
80
81
|
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;
|
|
82
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
83
|
+
if (this.config.fishermanMode) {
|
|
84
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
85
|
+
}
|
|
81
86
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
82
87
|
// Initialize config
|
|
83
88
|
this.updateConfig(this.config);
|
|
@@ -214,25 +219,50 @@ export { SequencerState };
|
|
|
214
219
|
// Check that we are a proposer for the next slot
|
|
215
220
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
216
221
|
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
217
|
-
// If we are not a proposer
|
|
222
|
+
// If we are not a proposer check if we should invalidate a invalid block, and bail
|
|
218
223
|
if (!canPropose) {
|
|
219
224
|
await this.considerInvalidatingBlock(syncedTo, slot);
|
|
220
225
|
return;
|
|
221
226
|
}
|
|
227
|
+
// In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
|
|
228
|
+
if (this.config.fishermanMode) {
|
|
229
|
+
if (this.lastSlotForValidationBlock === slot) {
|
|
230
|
+
this.log.trace(`Already validated block building for slot ${slot} (skipping)`, {
|
|
231
|
+
slot
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.log.debug(`Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`, {
|
|
236
|
+
slot,
|
|
237
|
+
proposer: proposer?.toString()
|
|
238
|
+
});
|
|
239
|
+
// Mark this slot as being validated
|
|
240
|
+
this.lastSlotForValidationBlock = slot;
|
|
241
|
+
}
|
|
222
242
|
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
223
243
|
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
224
244
|
this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
225
245
|
...syncLogData,
|
|
226
246
|
block: syncedTo.block.header.toInspect()
|
|
227
247
|
});
|
|
248
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
|
|
228
249
|
return;
|
|
229
250
|
}
|
|
230
251
|
// We now need to get ourselves a publisher.
|
|
231
252
|
// The returned attestor will be the one we provided if we provided one.
|
|
232
253
|
// Otherwise it will be a valid attestor for the returned publisher.
|
|
233
|
-
|
|
254
|
+
// In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
|
|
255
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(this.config.fishermanMode ? undefined : proposer);
|
|
234
256
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
235
257
|
this.publisher = publisher;
|
|
258
|
+
// In fisherman mode, set the actual proposer's address for simulations
|
|
259
|
+
if (this.config.fishermanMode) {
|
|
260
|
+
if (proposer) {
|
|
261
|
+
publisher.setProposerAddressForSimulation(proposer);
|
|
262
|
+
this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Get proposer credentials
|
|
236
266
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
|
|
237
267
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
|
|
238
268
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
@@ -245,6 +275,7 @@ export { SequencerState };
|
|
|
245
275
|
this.emit('proposer-rollup-check-failed', {
|
|
246
276
|
reason: 'Rollup contract check failed'
|
|
247
277
|
});
|
|
278
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
248
279
|
return;
|
|
249
280
|
} else if (canProposeCheck.slot !== slot) {
|
|
250
281
|
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}.`, {
|
|
@@ -256,6 +287,7 @@ export { SequencerState };
|
|
|
256
287
|
this.emit('proposer-rollup-check-failed', {
|
|
257
288
|
reason: 'Slot mismatch'
|
|
258
289
|
});
|
|
290
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
259
291
|
return;
|
|
260
292
|
} else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
|
|
261
293
|
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}.`, {
|
|
@@ -267,6 +299,7 @@ export { SequencerState };
|
|
|
267
299
|
this.emit('proposer-rollup-check-failed', {
|
|
268
300
|
reason: 'Block mismatch'
|
|
269
301
|
});
|
|
302
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
270
303
|
return;
|
|
271
304
|
}
|
|
272
305
|
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
|
|
@@ -274,6 +307,7 @@ export { SequencerState };
|
|
|
274
307
|
});
|
|
275
308
|
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
|
|
276
309
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
310
|
+
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
277
311
|
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
|
|
278
312
|
// Enqueues block invalidation
|
|
279
313
|
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
@@ -284,18 +318,41 @@ export { SequencerState };
|
|
|
284
318
|
const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
|
|
285
319
|
// Wait until the voting promises have resolved, so all requests are enqueued
|
|
286
320
|
await Promise.all(votesPromises);
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
321
|
+
// In fisherman mode, we don't publish to L1
|
|
322
|
+
if (this.config.fishermanMode) {
|
|
323
|
+
// Clear pending requests
|
|
324
|
+
publisher.clearPendingRequests();
|
|
325
|
+
if (block) {
|
|
326
|
+
this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
|
|
327
|
+
blockNumber: newBlockNumber,
|
|
328
|
+
slot: Number(slot),
|
|
329
|
+
archive: block.archive.toString(),
|
|
330
|
+
txCount: block.body.txEffects.length
|
|
331
|
+
});
|
|
332
|
+
this.lastBlockPublished = block;
|
|
333
|
+
this.metrics.recordBlockProposalSuccess();
|
|
334
|
+
} else {
|
|
335
|
+
// Block building failed in fisherman mode
|
|
336
|
+
this.log.warn(`Validation block building FAILED for slot ${slot}`, {
|
|
337
|
+
blockNumber: newBlockNumber,
|
|
338
|
+
slot: Number(slot)
|
|
339
|
+
});
|
|
340
|
+
this.metrics.recordBlockProposalFailed('block_build_failed');
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
// Normal mode: send the tx to L1
|
|
344
|
+
const l1Response = await publisher.sendRequests();
|
|
345
|
+
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
346
|
+
if (proposedBlock) {
|
|
347
|
+
this.lastBlockPublished = block;
|
|
348
|
+
this.emit('block-published', {
|
|
349
|
+
blockNumber: newBlockNumber,
|
|
350
|
+
slot: Number(slot)
|
|
351
|
+
});
|
|
352
|
+
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
353
|
+
} else if (block) {
|
|
354
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
355
|
+
}
|
|
299
356
|
}
|
|
300
357
|
this.setState(SequencerState.IDLE, undefined);
|
|
301
358
|
}
|
|
@@ -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;
|
|
@@ -685,7 +752,18 @@ export { SequencerState };
|
|
|
685
752
|
});
|
|
686
753
|
return false;
|
|
687
754
|
}) : undefined;
|
|
688
|
-
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>
|
|
755
|
+
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>{
|
|
756
|
+
// Record metrics for fisherman mode
|
|
757
|
+
if (this.config.fishermanMode && actions.length > 0) {
|
|
758
|
+
this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
|
|
759
|
+
slot,
|
|
760
|
+
actionCount: actions.length
|
|
761
|
+
});
|
|
762
|
+
this.metrics.recordSlashingAttempt(actions.length);
|
|
763
|
+
}
|
|
764
|
+
// Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
|
|
765
|
+
return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
|
|
766
|
+
}).catch((err)=>{
|
|
689
767
|
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
690
768
|
slot
|
|
691
769
|
});
|
|
@@ -731,6 +809,13 @@ export { SequencerState };
|
|
|
731
809
|
undefined
|
|
732
810
|
];
|
|
733
811
|
}
|
|
812
|
+
// In fisherman mode, just return the current proposer
|
|
813
|
+
if (this.config.fishermanMode) {
|
|
814
|
+
return [
|
|
815
|
+
true,
|
|
816
|
+
proposer
|
|
817
|
+
];
|
|
818
|
+
}
|
|
734
819
|
const validatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
735
820
|
const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
|
|
736
821
|
if (!weAreProposer) {
|
|
@@ -842,7 +927,12 @@ export { SequencerState };
|
|
|
842
927
|
}
|
|
843
928
|
this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
|
|
844
929
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
845
|
-
|
|
930
|
+
if (!this.config.fishermanMode) {
|
|
931
|
+
await publisher.sendRequests();
|
|
932
|
+
} else {
|
|
933
|
+
this.log.info('Invalidating block in fisherman mode, clearing pending requests');
|
|
934
|
+
publisher.clearPendingRequests();
|
|
935
|
+
}
|
|
846
936
|
}
|
|
847
937
|
getSlotStartBuildTimestamp(slotNumber) {
|
|
848
938
|
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/sequencer-client",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
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": "2.1.
|
|
30
|
-
"@aztec/bb-prover": "2.1.
|
|
31
|
-
"@aztec/blob-lib": "2.1.
|
|
32
|
-
"@aztec/blob-sink": "2.1.
|
|
33
|
-
"@aztec/constants": "2.1.
|
|
34
|
-
"@aztec/epoch-cache": "2.1.
|
|
35
|
-
"@aztec/ethereum": "2.1.
|
|
36
|
-
"@aztec/foundation": "2.1.
|
|
37
|
-
"@aztec/l1-artifacts": "2.1.
|
|
38
|
-
"@aztec/merkle-tree": "2.1.
|
|
39
|
-
"@aztec/node-keystore": "2.1.
|
|
40
|
-
"@aztec/noir-acvm_js": "2.1.
|
|
41
|
-
"@aztec/noir-contracts.js": "2.1.
|
|
42
|
-
"@aztec/noir-protocol-circuits-types": "2.1.
|
|
43
|
-
"@aztec/noir-types": "2.1.
|
|
44
|
-
"@aztec/p2p": "2.1.
|
|
45
|
-
"@aztec/protocol-contracts": "2.1.
|
|
46
|
-
"@aztec/prover-client": "2.1.
|
|
47
|
-
"@aztec/simulator": "2.1.
|
|
48
|
-
"@aztec/slasher": "2.1.
|
|
49
|
-
"@aztec/stdlib": "2.1.
|
|
50
|
-
"@aztec/telemetry-client": "2.1.
|
|
51
|
-
"@aztec/validator-client": "2.1.
|
|
52
|
-
"@aztec/world-state": "2.1.
|
|
29
|
+
"@aztec/aztec.js": "2.1.8",
|
|
30
|
+
"@aztec/bb-prover": "2.1.8",
|
|
31
|
+
"@aztec/blob-lib": "2.1.8",
|
|
32
|
+
"@aztec/blob-sink": "2.1.8",
|
|
33
|
+
"@aztec/constants": "2.1.8",
|
|
34
|
+
"@aztec/epoch-cache": "2.1.8",
|
|
35
|
+
"@aztec/ethereum": "2.1.8",
|
|
36
|
+
"@aztec/foundation": "2.1.8",
|
|
37
|
+
"@aztec/l1-artifacts": "2.1.8",
|
|
38
|
+
"@aztec/merkle-tree": "2.1.8",
|
|
39
|
+
"@aztec/node-keystore": "2.1.8",
|
|
40
|
+
"@aztec/noir-acvm_js": "2.1.8",
|
|
41
|
+
"@aztec/noir-contracts.js": "2.1.8",
|
|
42
|
+
"@aztec/noir-protocol-circuits-types": "2.1.8",
|
|
43
|
+
"@aztec/noir-types": "2.1.8",
|
|
44
|
+
"@aztec/p2p": "2.1.8",
|
|
45
|
+
"@aztec/protocol-contracts": "2.1.8",
|
|
46
|
+
"@aztec/prover-client": "2.1.8",
|
|
47
|
+
"@aztec/simulator": "2.1.8",
|
|
48
|
+
"@aztec/slasher": "2.1.8",
|
|
49
|
+
"@aztec/stdlib": "2.1.8",
|
|
50
|
+
"@aztec/telemetry-client": "2.1.8",
|
|
51
|
+
"@aztec/validator-client": "2.1.8",
|
|
52
|
+
"@aztec/world-state": "2.1.8",
|
|
53
53
|
"lodash.chunk": "^4.2.0",
|
|
54
54
|
"tslib": "^2.4.0",
|
|
55
|
-
"viem": "npm:@
|
|
55
|
+
"viem": "npm:@aztec/viem@2.38.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@aztec/archiver": "2.1.
|
|
59
|
-
"@aztec/kv-store": "2.1.
|
|
58
|
+
"@aztec/archiver": "2.1.8",
|
|
59
|
+
"@aztec/kv-store": "2.1.8",
|
|
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
|
@@ -149,6 +149,12 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
|
|
|
149
149
|
description: 'Inject a fake attestation (for testing only)',
|
|
150
150
|
...booleanConfigHelper(false),
|
|
151
151
|
},
|
|
152
|
+
fishermanMode: {
|
|
153
|
+
env: 'FISHERMAN_MODE',
|
|
154
|
+
description:
|
|
155
|
+
'Whether to run in fisherman mode: builds blocks on every slot for validation without publishing to L1',
|
|
156
|
+
...booleanConfigHelper(false),
|
|
157
|
+
},
|
|
152
158
|
shuffleAttestationOrdering: {
|
|
153
159
|
description: 'Shuffle attestation ordering to create invalid ordering (for testing only)',
|
|
154
160
|
...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';
|
|
@@ -39,7 +40,7 @@ import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
|
|
|
39
40
|
import { type ProposedBlockHeader, StateReference } from '@aztec/stdlib/tx';
|
|
40
41
|
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
41
42
|
|
|
42
|
-
import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
43
|
+
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
43
44
|
|
|
44
45
|
import type { PublisherConfig, TxSenderConfig } from './config.js';
|
|
45
46
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
@@ -113,6 +114,9 @@ export class SequencerPublisher {
|
|
|
113
114
|
protected ethereumSlotDuration: bigint;
|
|
114
115
|
|
|
115
116
|
private blobSinkClient: BlobSinkClientInterface;
|
|
117
|
+
|
|
118
|
+
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
119
|
+
private proposerAddressForSimulation?: EthAddress;
|
|
116
120
|
// @note - with blobs, the below estimate seems too large.
|
|
117
121
|
// Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
|
|
118
122
|
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
@@ -182,6 +186,14 @@ export class SequencerPublisher {
|
|
|
182
186
|
return this.l1TxUtils.getSenderAddress();
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Sets the proposer address to use for simulations in fisherman mode.
|
|
191
|
+
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
192
|
+
*/
|
|
193
|
+
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
194
|
+
this.proposerAddressForSimulation = proposerAddress;
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
public addRequest(request: RequestWithExpiry) {
|
|
186
198
|
this.requests.push(request);
|
|
187
199
|
}
|
|
@@ -190,6 +202,17 @@ export class SequencerPublisher {
|
|
|
190
202
|
return this.epochCache.getEpochAndSlotNow().slot;
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Clears all pending requests without sending them.
|
|
207
|
+
*/
|
|
208
|
+
public clearPendingRequests(): void {
|
|
209
|
+
const count = this.requests.length;
|
|
210
|
+
this.requests = [];
|
|
211
|
+
if (count > 0) {
|
|
212
|
+
this.log.debug(`Cleared ${count} pending request(s)`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
193
216
|
/**
|
|
194
217
|
* Sends all requests that are still valid.
|
|
195
218
|
* @returns one of:
|
|
@@ -355,10 +378,20 @@ export class SequencerPublisher {
|
|
|
355
378
|
] as const;
|
|
356
379
|
|
|
357
380
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
381
|
+
const stateOverrides = await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber);
|
|
382
|
+
let balance = 0n;
|
|
383
|
+
if (this.config.fishermanMode) {
|
|
384
|
+
// In fisherman mode, we can't know where the proposer is publishing from
|
|
385
|
+
// so we just add sufficient balance to the multicall3 address
|
|
386
|
+
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
387
|
+
} else {
|
|
388
|
+
balance = await this.l1TxUtils.getSenderBalance();
|
|
389
|
+
}
|
|
390
|
+
stateOverrides.push({
|
|
391
|
+
address: MULTI_CALL_3_ADDRESS,
|
|
392
|
+
balance,
|
|
393
|
+
});
|
|
358
394
|
|
|
359
|
-
// use sender balance to simulate
|
|
360
|
-
const balance = await this.l1TxUtils.getSenderBalance();
|
|
361
|
-
this.log.debug(`Simulating validateHeader with balance: ${balance}`);
|
|
362
395
|
await this.l1TxUtils.simulate(
|
|
363
396
|
{
|
|
364
397
|
to: this.rollupContract.address,
|
|
@@ -366,10 +399,7 @@ export class SequencerPublisher {
|
|
|
366
399
|
from: MULTI_CALL_3_ADDRESS,
|
|
367
400
|
},
|
|
368
401
|
{ time: ts + 1n },
|
|
369
|
-
|
|
370
|
-
{ address: MULTI_CALL_3_ADDRESS, balance },
|
|
371
|
-
...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
|
|
372
|
-
],
|
|
402
|
+
stateOverrides,
|
|
373
403
|
);
|
|
374
404
|
this.log.debug(`Simulated validateHeader`);
|
|
375
405
|
}
|
|
@@ -911,29 +941,39 @@ export class SequencerPublisher {
|
|
|
911
941
|
const kzg = Blob.getViemKzgInstance();
|
|
912
942
|
const blobInput = Blob.getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
913
943
|
this.log.debug('Validating blob input', { blobInput });
|
|
914
|
-
const blobEvaluationGas = await this.l1TxUtils
|
|
915
|
-
.estimateGas(
|
|
916
|
-
this.getSenderAddress().toString(),
|
|
917
|
-
{
|
|
918
|
-
to: this.rollupContract.address,
|
|
919
|
-
data: encodeFunctionData({
|
|
920
|
-
abi: RollupAbi,
|
|
921
|
-
functionName: 'validateBlobs',
|
|
922
|
-
args: [blobInput],
|
|
923
|
-
}),
|
|
924
|
-
},
|
|
925
|
-
{},
|
|
926
|
-
{
|
|
927
|
-
blobs: encodedData.blobs.map(b => b.data),
|
|
928
|
-
kzg,
|
|
929
|
-
},
|
|
930
|
-
)
|
|
931
|
-
.catch(err => {
|
|
932
|
-
const { message, metaMessages } = formatViemError(err);
|
|
933
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
934
|
-
throw new Error('Failed to validate blobs');
|
|
935
|
-
});
|
|
936
944
|
|
|
945
|
+
// Get blob evaluation gas
|
|
946
|
+
let blobEvaluationGas: bigint;
|
|
947
|
+
if (this.config.fishermanMode) {
|
|
948
|
+
// In fisherman mode, we can't estimate blob gas because estimateGas doesn't support state overrides
|
|
949
|
+
// Use a fixed estimate.
|
|
950
|
+
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
951
|
+
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
952
|
+
} else {
|
|
953
|
+
// Normal mode - use estimateGas with blob inputs
|
|
954
|
+
blobEvaluationGas = await this.l1TxUtils
|
|
955
|
+
.estimateGas(
|
|
956
|
+
this.getSenderAddress().toString(),
|
|
957
|
+
{
|
|
958
|
+
to: this.rollupContract.address,
|
|
959
|
+
data: encodeFunctionData({
|
|
960
|
+
abi: RollupAbi,
|
|
961
|
+
functionName: 'validateBlobs',
|
|
962
|
+
args: [blobInput],
|
|
963
|
+
}),
|
|
964
|
+
},
|
|
965
|
+
{},
|
|
966
|
+
{
|
|
967
|
+
blobs: encodedData.blobs.map(b => b.data),
|
|
968
|
+
kzg,
|
|
969
|
+
},
|
|
970
|
+
)
|
|
971
|
+
.catch(err => {
|
|
972
|
+
const { message, metaMessages } = formatViemError(err);
|
|
973
|
+
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
974
|
+
throw new Error('Failed to validate blobs');
|
|
975
|
+
});
|
|
976
|
+
}
|
|
937
977
|
const signers = encodedData.attestationsAndSigners.getSigners().map(signer => signer.toString());
|
|
938
978
|
|
|
939
979
|
const args = [
|
|
@@ -994,12 +1034,31 @@ export class SequencerPublisher {
|
|
|
994
1034
|
: []
|
|
995
1035
|
).flatMap(override => override.stateDiff ?? []);
|
|
996
1036
|
|
|
1037
|
+
const stateOverrides: StateOverride = [
|
|
1038
|
+
{
|
|
1039
|
+
address: this.rollupContract.address,
|
|
1040
|
+
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1041
|
+
stateDiff: [
|
|
1042
|
+
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1043
|
+
...forcePendingBlockNumberStateDiff,
|
|
1044
|
+
],
|
|
1045
|
+
},
|
|
1046
|
+
];
|
|
1047
|
+
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1048
|
+
if (this.proposerAddressForSimulation) {
|
|
1049
|
+
stateOverrides.push({
|
|
1050
|
+
address: this.proposerAddressForSimulation.toString(),
|
|
1051
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
997
1055
|
const simulationResult = await this.l1TxUtils
|
|
998
1056
|
.simulate(
|
|
999
1057
|
{
|
|
1000
1058
|
to: this.rollupContract.address,
|
|
1001
1059
|
data: rollupData,
|
|
1002
1060
|
gas: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
1061
|
+
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1003
1062
|
},
|
|
1004
1063
|
{
|
|
1005
1064
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
@@ -1007,16 +1066,7 @@ export class SequencerPublisher {
|
|
|
1007
1066
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1008
1067
|
gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
|
|
1009
1068
|
},
|
|
1010
|
-
|
|
1011
|
-
{
|
|
1012
|
-
address: this.rollupContract.address,
|
|
1013
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1014
|
-
stateDiff: [
|
|
1015
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1016
|
-
...forcePendingBlockNumberStateDiff,
|
|
1017
|
-
],
|
|
1018
|
-
},
|
|
1019
|
-
],
|
|
1069
|
+
stateOverrides,
|
|
1020
1070
|
RollupAbi,
|
|
1021
1071
|
{
|
|
1022
1072
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
@@ -1024,7 +1074,17 @@ export class SequencerPublisher {
|
|
|
1024
1074
|
},
|
|
1025
1075
|
)
|
|
1026
1076
|
.catch(err => {
|
|
1027
|
-
|
|
1077
|
+
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1078
|
+
const viemError = formatViemError(err);
|
|
1079
|
+
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1080
|
+
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1081
|
+
// Return a minimal simulation result with the fallback gas estimate
|
|
1082
|
+
return {
|
|
1083
|
+
gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
1084
|
+
logs: [],
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1028
1088
|
throw err;
|
|
1029
1089
|
});
|
|
1030
1090
|
|
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
|
}
|