@aztec/sequencer-client 1.2.1 → 2.0.0-nightly.20250814

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.
@@ -20,22 +20,24 @@ import {
20
20
  type ViemHeader,
21
21
  type ViemStateReference,
22
22
  formatViemError,
23
+ tryExtractEvent,
23
24
  } from '@aztec/ethereum';
24
25
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
26
  import { sumBigint } from '@aztec/foundation/bigint';
26
27
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
27
28
  import { EthAddress } from '@aztec/foundation/eth-address';
29
+ import type { Fr } from '@aztec/foundation/fields';
28
30
  import { createLogger } from '@aztec/foundation/log';
29
- import { Timer } from '@aztec/foundation/timer';
30
- import { RollupAbi } from '@aztec/l1-artifacts';
31
- import { CommitteeAttestation } from '@aztec/stdlib/block';
31
+ import { DateProvider, Timer } from '@aztec/foundation/timer';
32
+ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
33
+ import { CommitteeAttestation, type ValidateBlockResult } from '@aztec/stdlib/block';
32
34
  import { ConsensusPayload, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
33
35
  import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
34
36
  import { type ProposedBlockHeader, StateReference, TxHash } from '@aztec/stdlib/tx';
35
37
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
36
38
 
37
39
  import pick from 'lodash.pick';
38
- import { type TransactionReceipt, encodeFunctionData, multicall3Abi, toHex } from 'viem';
40
+ import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
39
41
 
40
42
  import type { PublisherConfig, TxSenderConfig } from './config.js';
41
43
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
@@ -56,29 +58,43 @@ type L1ProcessArgs = {
56
58
  attestations?: CommitteeAttestation[];
57
59
  };
58
60
 
59
- export enum VoteType {
61
+ export enum SignalType {
60
62
  GOVERNANCE,
61
63
  SLASHING,
62
64
  }
63
65
 
64
66
  type GetSlashPayloadCallBack = (slotNumber: bigint) => Promise<EthAddress | undefined>;
65
67
 
66
- const Actions = ['propose', 'governance-vote', 'slashing-vote'] as const;
68
+ const Actions = [
69
+ 'propose',
70
+ 'governance-signal',
71
+ 'slashing-signal',
72
+ 'invalidate-by-invalid-attestation',
73
+ 'invalidate-by-insufficient-attestations',
74
+ ] as const;
67
75
  export type Action = (typeof Actions)[number];
68
76
 
69
- // Sorting for actions such that proposals always go first
77
+ // Sorting for actions such that invalidations go first, then proposals, and last votes
70
78
  const compareActions = (a: Action, b: Action) => Actions.indexOf(b) - Actions.indexOf(a);
71
79
 
80
+ export type InvalidateBlockRequest = {
81
+ request: L1TxRequest;
82
+ reason: 'invalid-attestation' | 'insufficient-attestations';
83
+ gasUsed: bigint;
84
+ blockNumber: number;
85
+ forcePendingBlockNumber: number;
86
+ };
87
+
72
88
  interface RequestWithExpiry {
73
89
  action: Action;
74
90
  request: L1TxRequest;
75
91
  lastValidL2Slot: bigint;
76
92
  gasConfig?: Pick<L1GasConfig, 'txTimeoutAt' | 'gasLimit'>;
77
93
  blobConfig?: L1BlobInputs;
78
- onResult?: (
94
+ checkSuccess: (
79
95
  request: L1TxRequest,
80
96
  result?: { receipt: TransactionReceipt; gasPrice: GasPrice; stats?: TransactionStats; errorMsg?: string },
81
- ) => void;
97
+ ) => boolean;
82
98
  }
83
99
 
84
100
  export class SequencerPublisher {
@@ -87,16 +103,14 @@ export class SequencerPublisher {
87
103
  public epochCache: EpochCache;
88
104
 
89
105
  protected governanceLog = createLogger('sequencer:publisher:governance');
90
- protected governanceProposerAddress?: EthAddress;
91
106
  private governancePayload: EthAddress = EthAddress.ZERO;
92
107
 
93
108
  protected slashingLog = createLogger('sequencer:publisher:slashing');
94
- protected slashingProposerAddress?: EthAddress;
95
109
  private getSlashPayload?: GetSlashPayloadCallBack = undefined;
96
110
 
97
- private myLastVotes: Record<VoteType, bigint> = {
98
- [VoteType.GOVERNANCE]: 0n,
99
- [VoteType.SLASHING]: 0n,
111
+ private myLastSignals: Record<SignalType, bigint> = {
112
+ [SignalType.GOVERNANCE]: 0n,
113
+ [SignalType.SLASHING]: 0n,
100
114
  };
101
115
 
102
116
  protected log = createLogger('sequencer:publisher');
@@ -131,6 +145,7 @@ export class SequencerPublisher {
131
145
  slashingProposerContract: SlashingProposerContract;
132
146
  governanceProposerContract: GovernanceProposerContract;
133
147
  epochCache: EpochCache;
148
+ dateProvider: DateProvider;
134
149
  },
135
150
  ) {
136
151
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
@@ -147,6 +162,12 @@ export class SequencerPublisher {
147
162
 
148
163
  this.govProposerContract = deps.governanceProposerContract;
149
164
  this.slashingProposerContract = deps.slashingProposerContract;
165
+
166
+ this.rollupContract.listenToSlasherChanged(async () => {
167
+ this.log.info('Slashing proposer changed');
168
+ const newSlashingProposer = await this.rollupContract.getSlashingProposer();
169
+ this.slashingProposerContract = newSlashingProposer;
170
+ });
150
171
  }
151
172
 
152
173
  public getRollupContract(): RollupContract {
@@ -240,9 +261,7 @@ export class SequencerPublisher {
240
261
  validRequests.sort((a, b) => compareActions(a.action, b.action));
241
262
 
242
263
  try {
243
- this.log.debug('Forwarding transactions', {
244
- validRequests: validRequests.map(request => request.action),
245
- });
264
+ this.log.debug('Forwarding transactions', { validRequests: validRequests.map(request => request.action) });
246
265
  const result = await Multicall3.forward(
247
266
  validRequests.map(request => request.request),
248
267
  this.l1TxUtils,
@@ -251,8 +270,8 @@ export class SequencerPublisher {
251
270
  this.rollupContract.address,
252
271
  this.log,
253
272
  );
254
- this.callbackBundledTransactions(validRequests, result);
255
- return { result, expiredActions, validActions };
273
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
274
+ return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
256
275
  } catch (err) {
257
276
  const viemError = formatViemError(err);
258
277
  this.log.error(`Failed to publish bundled transactions`, viemError);
@@ -270,17 +289,22 @@ export class SequencerPublisher {
270
289
  requests: RequestWithExpiry[],
271
290
  result?: { receipt: TransactionReceipt; gasPrice: GasPrice } | FormattedViemError,
272
291
  ) {
273
- const isError = result instanceof FormattedViemError;
274
- const success = isError ? false : result?.receipt.status === 'success';
275
- const logger = success ? this.log.info : this.log.error;
276
- for (const request of requests) {
277
- logger(`Bundled [${request.action}] transaction [${success ? 'succeeded' : 'failed'}]`);
278
- if (!isError) {
279
- request.onResult?.(request.request, result);
292
+ const actionsListStr = requests.map(r => r.action).join(', ');
293
+ if (result instanceof FormattedViemError) {
294
+ this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
295
+ return { failedActions: requests.map(r => r.action) };
296
+ } else {
297
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
298
+ const successfulActions: Action[] = [];
299
+ const failedActions: Action[] = [];
300
+ for (const request of requests) {
301
+ if (request.checkSuccess(request.request, result)) {
302
+ successfulActions.push(request.action);
303
+ } else {
304
+ failedActions.push(request.action);
305
+ }
280
306
  }
281
- }
282
- if (isError) {
283
- this.log.error('Failed to publish bundled transactions', result);
307
+ return { successfulActions, failedActions };
284
308
  }
285
309
  }
286
310
 
@@ -289,12 +313,16 @@ export class SequencerPublisher {
289
313
  * @param tipArchive - The archive to check
290
314
  * @returns The slot and block number if it is possible to propose, undefined otherwise
291
315
  */
292
- public canProposeAtNextEthBlock(tipArchive: Buffer, msgSender: EthAddress) {
316
+ public canProposeAtNextEthBlock(
317
+ tipArchive: Fr,
318
+ msgSender: EthAddress,
319
+ opts: { forcePendingBlockNumber?: number } = {},
320
+ ) {
293
321
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
294
322
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
295
323
 
296
324
  return this.rollupContract
297
- .canProposeAtNextEthBlock(tipArchive, msgSender.toString(), this.ethereumSlotDuration)
325
+ .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, opts)
298
326
  .catch(err => {
299
327
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
300
328
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -312,12 +340,16 @@ export class SequencerPublisher {
312
340
  * It will throw if the block header is invalid.
313
341
  * @param header - The block header to validate
314
342
  */
315
- public async validateBlockHeader(header: ProposedBlockHeader) {
343
+ public async validateBlockHeader(
344
+ header: ProposedBlockHeader,
345
+ opts?: { forcePendingBlockNumber: number | undefined },
346
+ ) {
316
347
  const flags = { ignoreDA: true, ignoreSignatures: true };
317
348
 
318
349
  const args = [
319
350
  header.toViem(),
320
351
  RollupContract.packAttestations([]),
352
+ [], // no signers
321
353
  `0x${'0'.repeat(64)}`, // 32 empty bytes
322
354
  header.contentCommitment.blobsHash.toString(),
323
355
  flags,
@@ -330,21 +362,109 @@ export class SequencerPublisher {
330
362
  await this.l1TxUtils.simulate(
331
363
  {
332
364
  to: this.rollupContract.address,
333
- data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeader', args }),
365
+ data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
334
366
  from: MULTI_CALL_3_ADDRESS,
335
367
  },
336
- {
337
- time: ts + 1n,
338
- },
368
+ { time: ts + 1n },
339
369
  [
340
- {
341
- address: MULTI_CALL_3_ADDRESS,
342
- balance,
343
- },
370
+ { address: MULTI_CALL_3_ADDRESS, balance },
371
+ ...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
344
372
  ],
345
373
  );
346
374
  }
347
375
 
376
+ /**
377
+ * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
378
+ * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
379
+ */
380
+ public async simulateInvalidateBlock(
381
+ validationResult: ValidateBlockResult,
382
+ ): Promise<InvalidateBlockRequest | undefined> {
383
+ if (validationResult.valid) {
384
+ return undefined;
385
+ }
386
+
387
+ const { reason, block } = validationResult;
388
+ const blockNumber = block.block.number;
389
+ const logData = { ...block.block.toBlockInfo(), reason };
390
+
391
+ const currentBlockNumber = await this.rollupContract.getBlockNumber();
392
+ if (currentBlockNumber < validationResult.block.block.number) {
393
+ this.log.verbose(
394
+ `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
395
+ { currentBlockNumber, ...logData },
396
+ );
397
+ return undefined;
398
+ }
399
+
400
+ const request = this.buildInvalidateBlockRequest(validationResult);
401
+ this.log.debug(`Simulating invalidate block ${blockNumber}`, logData);
402
+
403
+ try {
404
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
405
+ this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
406
+
407
+ return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
408
+ } catch (err) {
409
+ const viemError = formatViemError(err);
410
+
411
+ // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
412
+ // we can safely ignore it and return undefined so we go ahead with block building.
413
+ if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
414
+ this.log.verbose(
415
+ `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
416
+ { ...logData, request, error: viemError.message },
417
+ );
418
+ const latestPendingBlockNumber = await this.rollupContract.getBlockNumber();
419
+ if (latestPendingBlockNumber < blockNumber) {
420
+ this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
421
+ return undefined;
422
+ } else {
423
+ this.log.error(
424
+ `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
425
+ viemError,
426
+ logData,
427
+ );
428
+ throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
429
+ cause: viemError,
430
+ });
431
+ }
432
+ }
433
+
434
+ // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
435
+ this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
436
+ throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
437
+ }
438
+ }
439
+
440
+ private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
441
+ if (validationResult.valid) {
442
+ throw new Error('Cannot invalidate a valid block');
443
+ }
444
+
445
+ const { block, committee, reason } = validationResult;
446
+ const logData = { ...block.block.toBlockInfo(), reason };
447
+ this.log.debug(`Simulating invalidate block ${block.block.number}`, logData);
448
+
449
+ if (reason === 'invalid-attestation') {
450
+ return this.rollupContract.buildInvalidateBadAttestationRequest(
451
+ block.block.number,
452
+ block.attestations.map(a => a.toViem()),
453
+ committee,
454
+ validationResult.invalidIndex,
455
+ );
456
+ } else if (reason === 'insufficient-attestations') {
457
+ return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
458
+ block.block.number,
459
+ block.attestations.map(a => a.toViem()),
460
+ committee,
461
+ );
462
+ } else {
463
+ const _: never = reason;
464
+ throw new Error(`Unknown reason for invalidation`);
465
+ }
466
+ }
467
+
348
468
  /**
349
469
  * @notice Will simulate `propose` to make sure that the block is valid for submission
350
470
  *
@@ -360,6 +480,7 @@ export class SequencerPublisher {
360
480
  digest: Buffer.alloc(32),
361
481
  attestations: [],
362
482
  },
483
+ options: { forcePendingBlockNumber?: number },
363
484
  ): Promise<bigint> {
364
485
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
365
486
 
@@ -376,13 +497,14 @@ export class SequencerPublisher {
376
497
  CommitteeAttestation.fromAddress(committeeMember),
377
498
  );
378
499
  }
379
- // const blobs = await Blob.getBlobs(block.body.toBlobFields());
380
- // const blobInput = Blob.getEthBlobEvaluationInputs(blobs);
381
500
 
382
501
  const blobs = await Blob.getBlobsPerBlock(block.body.toBlobFields());
383
502
  const blobInput = Blob.getPrefixedEthBlobCommitments(blobs);
384
503
 
385
504
  const formattedAttestations = attestationData.attestations.map(attest => attest.toViem());
505
+ const signers = attestationData.attestations
506
+ .filter(attest => !attest.signature.isEmpty())
507
+ .map(attest => attest.address.toString());
386
508
 
387
509
  const args = [
388
510
  {
@@ -395,47 +517,48 @@ export class SequencerPublisher {
395
517
  },
396
518
  },
397
519
  RollupContract.packAttestations(formattedAttestations),
520
+ signers,
398
521
  blobInput,
399
522
  ] as const;
400
523
 
401
- await this.simulateProposeTx(args, ts);
524
+ await this.simulateProposeTx(args, ts, options);
402
525
  return ts;
403
526
  }
404
527
 
405
- public async getCurrentEpochCommittee(): Promise<EthAddress[] | undefined> {
406
- const committee = await this.rollupContract.getCurrentEpochCommittee();
407
- return committee?.map(EthAddress.fromString);
408
- }
409
-
410
- private async enqueueCastVoteHelper(
528
+ private async enqueueCastSignalHelper(
411
529
  slotNumber: bigint,
412
530
  timestamp: bigint,
413
- voteType: VoteType,
531
+ signalType: SignalType,
414
532
  payload: EthAddress,
415
533
  base: IEmpireBase,
416
534
  signerAddress: EthAddress,
417
- signer: (msg: `0x${string}`) => Promise<`0x${string}`>,
535
+ signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
418
536
  ): Promise<boolean> {
419
- if (this.myLastVotes[voteType] >= slotNumber) {
537
+ if (this.myLastSignals[signalType] >= slotNumber) {
420
538
  return false;
421
539
  }
422
540
  if (payload.equals(EthAddress.ZERO)) {
423
541
  return false;
424
542
  }
543
+ if (signerAddress.equals(EthAddress.ZERO)) {
544
+ this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
545
+ return false;
546
+ }
425
547
  const round = await base.computeRound(slotNumber);
426
548
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
427
549
 
428
- if (roundInfo.lastVote >= slotNumber) {
550
+ if (roundInfo.lastSignalSlot >= slotNumber) {
429
551
  return false;
430
552
  }
431
553
 
432
- const cachedLastVote = this.myLastVotes[voteType];
433
- this.myLastVotes[voteType] = slotNumber;
554
+ const cachedLastVote = this.myLastSignals[signalType];
555
+ this.myLastSignals[signalType] = slotNumber;
434
556
 
435
- const action = voteType === VoteType.GOVERNANCE ? 'governance-vote' : 'slashing-vote';
557
+ const action = signalType === SignalType.GOVERNANCE ? 'governance-signal' : 'slashing-signal';
436
558
 
437
- const request = await base.createVoteRequestWithSignature(
559
+ const request = await base.createSignalRequestWithSignature(
438
560
  payload.toString(),
561
+ round,
439
562
  this.config.l1ChainId,
440
563
  signerAddress.toString(),
441
564
  signer,
@@ -447,29 +570,53 @@ export class SequencerPublisher {
447
570
  lastValidL2Slot: slotNumber,
448
571
  });
449
572
 
573
+ try {
574
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
575
+ this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
576
+ } catch (err) {
577
+ this.log.warn(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
578
+ // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
579
+ }
580
+
450
581
  this.addRequest({
451
582
  gasConfig: { gasLimit: SequencerPublisher.VOTE_GAS_GUESS },
452
583
  action,
453
584
  request,
454
585
  lastValidL2Slot: slotNumber,
455
- onResult: (_request, result) => {
456
- if (!result || result.receipt.status !== 'success') {
457
- this.myLastVotes[voteType] = cachedLastVote;
586
+ checkSuccess: (_request, result) => {
587
+ const success =
588
+ result &&
589
+ result.receipt &&
590
+ result.receipt.status === 'success' &&
591
+ tryExtractEvent(result.receipt.logs, base.address.toString(), EmpireBaseAbi, 'SignalCast');
592
+
593
+ const logData = { ...result, slotNumber, round, payload: payload.toString() };
594
+ if (!success) {
595
+ this.log.error(
596
+ `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
597
+ logData,
598
+ );
599
+ this.myLastSignals[signalType] = cachedLastVote;
600
+ return false;
458
601
  } else {
459
- this.log.info(`Voting in [${action}] for ${payload} at slot ${slotNumber} in round ${round}`);
602
+ this.log.info(
603
+ `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
604
+ logData,
605
+ );
606
+ return true;
460
607
  }
461
608
  },
462
609
  });
463
610
  return true;
464
611
  }
465
612
 
466
- private async getVoteConfig(
613
+ private async getSignalConfig(
467
614
  slotNumber: bigint,
468
- voteType: VoteType,
615
+ signalType: SignalType,
469
616
  ): Promise<{ payload: EthAddress; base: IEmpireBase } | undefined> {
470
- if (voteType === VoteType.GOVERNANCE) {
617
+ if (signalType === SignalType.GOVERNANCE) {
471
618
  return { payload: this.governancePayload, base: this.govProposerContract };
472
- } else if (voteType === VoteType.SLASHING) {
619
+ } else if (signalType === SignalType.SLASHING) {
473
620
  if (!this.getSlashPayload) {
474
621
  return undefined;
475
622
  }
@@ -479,34 +626,32 @@ export class SequencerPublisher {
479
626
  }
480
627
  this.log.info(`Slash payload: ${slashPayload}`);
481
628
  return { payload: slashPayload, base: this.slashingProposerContract };
629
+ } else {
630
+ const _: never = signalType;
631
+ throw new Error('Unreachable: Invalid signal type');
482
632
  }
483
- throw new Error('Unreachable: Invalid vote type');
484
633
  }
485
634
 
486
635
  /**
487
- * Enqueues a castVote transaction to cast a vote for a given slot number.
488
- * @param slotNumber - The slot number to cast a vote for.
489
- * @param timestamp - The timestamp of the slot to cast a vote for.
490
- * @param voteType - The type of vote to cast.
491
- * @returns True if the vote was successfully enqueued, false otherwise.
636
+ * Enqueues a castSignal transaction to cast a signal for a given slot number.
637
+ * @param slotNumber - The slot number to cast a signal for.
638
+ * @param timestamp - The timestamp of the slot to cast a signal for.
639
+ * @param signalType - The type of signal to cast.
640
+ * @returns True if the signal was successfully enqueued, false otherwise.
492
641
  */
493
- public async enqueueCastVote(
642
+ public async enqueueCastSignal(
494
643
  slotNumber: bigint,
495
644
  timestamp: bigint,
496
- voteType: VoteType,
645
+ signalType: SignalType,
497
646
  signerAddress: EthAddress,
498
- signer: (msg: `0x${string}`) => Promise<`0x${string}`>,
647
+ signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
499
648
  ): Promise<boolean> {
500
- const voteConfig = await this.getVoteConfig(slotNumber, voteType);
501
- if (!voteConfig) {
649
+ const signalConfig = await this.getSignalConfig(slotNumber, signalType);
650
+ if (!signalConfig) {
502
651
  return false;
503
652
  }
504
- if (signerAddress.equals(EthAddress.ZERO)) {
505
- this.log.warn(`Cannot enqueue vote cast signal ${voteType} for address zero at slot ${slotNumber}`);
506
- return false;
507
- }
508
- const { payload, base } = voteConfig;
509
- return this.enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base, signerAddress, signer);
653
+ const { payload, base } = signalConfig;
654
+ return this.enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer);
510
655
  }
511
656
 
512
657
  /**
@@ -519,7 +664,7 @@ export class SequencerPublisher {
519
664
  block: L2Block,
520
665
  attestations?: CommitteeAttestation[],
521
666
  txHashes?: TxHash[],
522
- opts: { txTimeoutAt?: Date } = {},
667
+ opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
523
668
  ): Promise<boolean> {
524
669
  const proposedBlockHeader = block.header.toPropose();
525
670
 
@@ -544,23 +689,54 @@ export class SequencerPublisher {
544
689
  // This means that we can avoid the simulation issues in later checks.
545
690
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
546
691
  // make time consistency checks break.
547
- ts = await this.validateBlockForSubmission(block, {
548
- digest: digest.toBuffer(),
549
- attestations: attestations ?? [],
550
- });
692
+ const attestationData = { digest: digest.toBuffer(), attestations: attestations ?? [] };
693
+ // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
694
+ ts = await this.validateBlockForSubmission(block, attestationData, opts);
551
695
  } catch (err: any) {
552
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, undefined, {
696
+ this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
553
697
  ...block.getStats(),
554
698
  slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
699
+ forcePendingBlockNumber: opts.forcePendingBlockNumber,
555
700
  });
556
701
  throw err;
557
702
  }
558
703
 
559
- this.log.debug(`Submitting propose transaction`);
704
+ this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
560
705
  await this.addProposeTx(block, proposeTxArgs, opts, ts);
561
706
  return true;
562
707
  }
563
708
 
709
+ public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
710
+ if (!request) {
711
+ return;
712
+ }
713
+
714
+ // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
715
+ const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
716
+
717
+ const logData = { ...pick(request, 'gasUsed', 'blockNumber'), gasLimit, opts };
718
+ this.log.verbose(`Enqueuing invalidate block request`, logData);
719
+ this.addRequest({
720
+ action: `invalidate-by-${request.reason}`,
721
+ request: request.request,
722
+ gasConfig: { gasLimit, txTimeoutAt: opts.txTimeoutAt },
723
+ lastValidL2Slot: this.getCurrentL2Slot() + 2n,
724
+ checkSuccess: (_req, result) => {
725
+ const success =
726
+ result &&
727
+ result.receipt &&
728
+ result.receipt.status === 'success' &&
729
+ tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'BlockInvalidated');
730
+ if (!success) {
731
+ this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
732
+ } else {
733
+ this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
734
+ }
735
+ return !!success;
736
+ },
737
+ });
738
+ }
739
+
564
740
  /**
565
741
  * Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
566
742
  * Be warned, the call may return false even if the tx subsequently gets successfully mined.
@@ -578,7 +754,11 @@ export class SequencerPublisher {
578
754
  this.l1TxUtils.restart();
579
755
  }
580
756
 
581
- private async prepareProposeTx(encodedData: L1ProcessArgs, timestamp: bigint) {
757
+ private async prepareProposeTx(
758
+ encodedData: L1ProcessArgs,
759
+ timestamp: bigint,
760
+ options: { forcePendingBlockNumber?: number },
761
+ ) {
582
762
  if (!this.l1TxUtils.client.account) {
583
763
  throw new Error('L1 TX utils needs to be initialized with an account wallet.');
584
764
  }
@@ -610,6 +790,11 @@ export class SequencerPublisher {
610
790
 
611
791
  const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViem()) : [];
612
792
  const txHashes = encodedData.txHashes ? encodedData.txHashes.map(txHash => txHash.toString()) : [];
793
+
794
+ const signers = encodedData.attestations
795
+ ?.filter(attest => !attest.signature.isEmpty())
796
+ .map(attest => attest.address.toString());
797
+
613
798
  const args = [
614
799
  {
615
800
  header: encodedData.header.toViem(),
@@ -622,10 +807,11 @@ export class SequencerPublisher {
622
807
  txHashes,
623
808
  },
624
809
  RollupContract.packAttestations(attestations),
810
+ signers ?? [],
625
811
  blobInput,
626
812
  ] as const;
627
813
 
628
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp);
814
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
629
815
 
630
816
  return { args, blobEvaluationGas, rollupData, simulationResult };
631
817
  }
@@ -648,9 +834,11 @@ export class SequencerPublisher {
648
834
  };
649
835
  },
650
836
  ViemCommitteeAttestations,
837
+ `0x${string}`[],
651
838
  `0x${string}`,
652
839
  ],
653
840
  timestamp: bigint,
841
+ options: { forcePendingBlockNumber?: number },
654
842
  ) {
655
843
  const rollupData = encodeFunctionData({
656
844
  abi: RollupAbi,
@@ -658,17 +846,18 @@ export class SequencerPublisher {
658
846
  args,
659
847
  });
660
848
 
661
- const forwarderData = encodeFunctionData({
662
- abi: multicall3Abi,
663
- functionName: 'aggregate3',
664
- args: [[{ target: this.rollupContract.address, allowFailure: false, callData: rollupData }]],
665
- });
849
+ // override the pending block number if requested
850
+ const forcePendingBlockNumberStateDiff = (
851
+ options.forcePendingBlockNumber !== undefined
852
+ ? await this.rollupContract.makePendingBlockNumberOverride(options.forcePendingBlockNumber)
853
+ : []
854
+ ).flatMap(override => override.stateDiff ?? []);
666
855
 
667
856
  const simulationResult = await this.l1TxUtils
668
857
  .simulate(
669
858
  {
670
- to: MULTI_CALL_3_ADDRESS,
671
- data: forwarderData,
859
+ to: this.rollupContract.address,
860
+ data: rollupData,
672
861
  gas: SequencerPublisher.PROPOSE_GAS_GUESS,
673
862
  },
674
863
  {
@@ -682,10 +871,8 @@ export class SequencerPublisher {
682
871
  address: this.rollupContract.address,
683
872
  // @note we override checkBlob to false since blobs are not part simulate()
684
873
  stateDiff: [
685
- {
686
- slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
687
- value: toPaddedHex(0n, true),
688
- },
874
+ { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
875
+ ...forcePendingBlockNumberStateDiff,
689
876
  ],
690
877
  },
691
878
  ],
@@ -706,13 +893,28 @@ export class SequencerPublisher {
706
893
  private async addProposeTx(
707
894
  block: L2Block,
708
895
  encodedData: L1ProcessArgs,
709
- opts: { txTimeoutAt?: Date } = {},
896
+ opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
710
897
  timestamp: bigint,
711
898
  ): Promise<void> {
712
899
  const timer = new Timer();
713
900
  const kzg = Blob.getViemKzgInstance();
714
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp);
901
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
902
+ encodedData,
903
+ timestamp,
904
+ opts,
905
+ );
715
906
  const startBlock = await this.l1TxUtils.getBlockNumber();
907
+ const gasLimit = this.l1TxUtils.bumpGasLimit(
908
+ BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
909
+ blobEvaluationGas +
910
+ SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
911
+ );
912
+
913
+ // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
914
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
915
+ void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch(_err => {
916
+ this.log.error('Failed to send blobs to blob sink');
917
+ });
716
918
 
717
919
  return this.addRequest({
718
920
  action: 'propose',
@@ -721,20 +923,21 @@ export class SequencerPublisher {
721
923
  data: rollupData,
722
924
  },
723
925
  lastValidL2Slot: block.header.globalVariables.slotNumber.toBigInt(),
724
- gasConfig: {
725
- ...opts,
726
- gasLimit: this.l1TxUtils.bumpGasLimit(simulationResult.gasUsed + blobEvaluationGas),
727
- },
926
+ gasConfig: { ...opts, gasLimit },
728
927
  blobConfig: {
729
928
  blobs: encodedData.blobs.map(b => b.data),
730
929
  kzg,
731
930
  },
732
- onResult: (request, result) => {
931
+ checkSuccess: (request, result) => {
733
932
  if (!result) {
734
- return;
933
+ return false;
735
934
  }
736
935
  const { receipt, stats, errorMsg } = result;
737
- if (receipt.status === 'success') {
936
+ const success =
937
+ receipt &&
938
+ receipt.status === 'success' &&
939
+ tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'L2BlockProposed');
940
+ if (success) {
738
941
  const endBlock = receipt.blockNumber;
739
942
  const inclusionBlocks = Number(endBlock - startBlock);
740
943
  const publishStats: L1PublishBlockStats = {
@@ -749,37 +952,21 @@ export class SequencerPublisher {
749
952
  blobCount: encodedData.blobs.length,
750
953
  inclusionBlocks,
751
954
  };
752
- this.log.verbose(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats() });
955
+ this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
753
956
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
754
957
 
755
- // Send the blobs to the blob sink
756
- this.sendBlobsToBlobSink(receipt.blockHash, encodedData.blobs).catch(_err => {
757
- this.log.error('Failed to send blobs to blob sink');
758
- });
759
-
760
958
  return true;
761
959
  } else {
762
960
  this.metrics.recordFailedTx('process');
763
-
764
- this.log.error(`Rollup process tx reverted. ${errorMsg ?? 'No error message'}`, undefined, {
961
+ this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
765
962
  ...block.getStats(),
963
+ receipt,
766
964
  txHash: receipt.transactionHash,
767
965
  slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
768
966
  });
967
+ return false;
769
968
  }
770
969
  },
771
970
  });
772
971
  }
773
-
774
- /**
775
- * Send blobs to the blob sink
776
- *
777
- * If a blob sink url is configured, then we send blobs to the blob sink
778
- * - for now we use the blockHash as the identifier for the blobs;
779
- * In the future this will move to be the beacon block id - which takes a bit more work
780
- * to calculate and will need to be mocked in e2e tests
781
- */
782
- protected sendBlobsToBlobSink(blockHash: string, blobs: Blob[]): Promise<boolean> {
783
- return this.blobSinkClient.sendBlobsToBlobSink(blockHash, blobs);
784
- }
785
972
  }