@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.
@@ -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, check if we should invalidate a invalid block, and bail
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
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
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
- // And send the tx to L1
288
- const l1Response = await publisher.sendRequests();
289
- const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
290
- if (proposedBlock) {
291
- this.lastBlockPublished = block;
292
- this.emit('block-published', {
293
- blockNumber: newBlockNumber,
294
- slot: Number(slot)
295
- });
296
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
297
- } else if (block) {
298
- this.emit('block-publish-failed', l1Response ?? {});
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
- 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;
@@ -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)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
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
- await publisher.sendRequests();
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.7",
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.7",
30
- "@aztec/bb-prover": "2.1.7",
31
- "@aztec/blob-lib": "2.1.7",
32
- "@aztec/blob-sink": "2.1.7",
33
- "@aztec/constants": "2.1.7",
34
- "@aztec/epoch-cache": "2.1.7",
35
- "@aztec/ethereum": "2.1.7",
36
- "@aztec/foundation": "2.1.7",
37
- "@aztec/l1-artifacts": "2.1.7",
38
- "@aztec/merkle-tree": "2.1.7",
39
- "@aztec/node-keystore": "2.1.7",
40
- "@aztec/noir-acvm_js": "2.1.7",
41
- "@aztec/noir-contracts.js": "2.1.7",
42
- "@aztec/noir-protocol-circuits-types": "2.1.7",
43
- "@aztec/noir-types": "2.1.7",
44
- "@aztec/p2p": "2.1.7",
45
- "@aztec/protocol-contracts": "2.1.7",
46
- "@aztec/prover-client": "2.1.7",
47
- "@aztec/simulator": "2.1.7",
48
- "@aztec/slasher": "2.1.7",
49
- "@aztec/stdlib": "2.1.7",
50
- "@aztec/telemetry-client": "2.1.7",
51
- "@aztec/validator-client": "2.1.7",
52
- "@aztec/world-state": "2.1.7",
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:@spalladino/viem@2.38.2-eip7594.2"
55
+ "viem": "npm:@aztec/viem@2.38.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@aztec/archiver": "2.1.7",
59
- "@aztec/kv-store": "2.1.7",
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),
@@ -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
- this.log.error(`Failed to simulate propose tx`, err);
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
 
@@ -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
  }