@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.
@@ -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, check if we should invalidate a invalid block, and bail
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
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
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
- // And send the tx to L1
289
- const l1Response = await publisher.sendRequests();
290
- const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
291
- if (proposedBlock) {
292
- this.lastBlockPublished = block;
293
- this.emit('block-published', {
294
- blockNumber: newBlockNumber,
295
- slot: Number(slot)
296
- });
297
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
298
- } else if (block) {
299
- this.emit('block-publish-failed', l1Response ?? {});
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
- this.log.debug('Collecting attestations');
480
- const attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
481
- this.log.verbose(`Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`, {
482
- blockHash,
483
- blockNumber,
484
- slot
485
- });
486
- const attestationsAndSignersSignature = await this.validatorClient?.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress()) ?? Signature.empty();
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)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
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
- await publisher.sendRequests();
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.20251111",
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.20251111",
30
- "@aztec/bb-prover": "3.0.0-nightly.20251111",
31
- "@aztec/blob-lib": "3.0.0-nightly.20251111",
32
- "@aztec/blob-sink": "3.0.0-nightly.20251111",
33
- "@aztec/constants": "3.0.0-nightly.20251111",
34
- "@aztec/epoch-cache": "3.0.0-nightly.20251111",
35
- "@aztec/ethereum": "3.0.0-nightly.20251111",
36
- "@aztec/foundation": "3.0.0-nightly.20251111",
37
- "@aztec/l1-artifacts": "3.0.0-nightly.20251111",
38
- "@aztec/merkle-tree": "3.0.0-nightly.20251111",
39
- "@aztec/node-keystore": "3.0.0-nightly.20251111",
40
- "@aztec/noir-acvm_js": "3.0.0-nightly.20251111",
41
- "@aztec/noir-contracts.js": "3.0.0-nightly.20251111",
42
- "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251111",
43
- "@aztec/noir-types": "3.0.0-nightly.20251111",
44
- "@aztec/p2p": "3.0.0-nightly.20251111",
45
- "@aztec/protocol-contracts": "3.0.0-nightly.20251111",
46
- "@aztec/prover-client": "3.0.0-nightly.20251111",
47
- "@aztec/simulator": "3.0.0-nightly.20251111",
48
- "@aztec/slasher": "3.0.0-nightly.20251111",
49
- "@aztec/stdlib": "3.0.0-nightly.20251111",
50
- "@aztec/telemetry-client": "3.0.0-nightly.20251111",
51
- "@aztec/validator-client": "3.0.0-nightly.20251111",
52
- "@aztec/world-state": "3.0.0-nightly.20251111",
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.20251111",
59
- "@aztec/kv-store": "3.0.0-nightly.20251111",
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),
@@ -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
- this.log.error(`Failed to simulate propose tx`, err);
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
 
@@ -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
  }