@aztec/sequencer-client 0.0.1-commit.5daedc8 → 0.0.1-commit.6201a7b05

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.
Files changed (136) hide show
  1. package/dest/client/sequencer-client.d.ts +26 -16
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +76 -36
  4. package/dest/config.d.ts +35 -9
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +113 -50
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +25 -17
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +44 -68
  16. package/dest/global_variable_builder/index.d.ts +4 -2
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/index.d.ts +2 -3
  20. package/dest/index.d.ts.map +1 -1
  21. package/dest/index.js +1 -2
  22. package/dest/publisher/config.d.ts +53 -20
  23. package/dest/publisher/config.d.ts.map +1 -1
  24. package/dest/publisher/config.js +124 -39
  25. package/dest/publisher/index.d.ts +2 -1
  26. package/dest/publisher/index.d.ts.map +1 -1
  27. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  28. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  30. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  31. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  32. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  33. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  34. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  35. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  36. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  37. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  38. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  39. package/dest/publisher/sequencer-publisher-factory.d.ts +15 -8
  40. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher-factory.js +28 -4
  42. package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
  43. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  44. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  45. package/dest/publisher/sequencer-publisher.d.ts +114 -80
  46. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  47. package/dest/publisher/sequencer-publisher.js +1010 -244
  48. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  49. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  50. package/dest/sequencer/chain_state_overrides.js +39 -0
  51. package/dest/sequencer/checkpoint_proposal_job.d.ts +138 -0
  52. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  53. package/dest/sequencer/checkpoint_proposal_job.js +1665 -0
  54. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  55. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  56. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  57. package/dest/sequencer/checkpoint_voter.d.ts +34 -0
  58. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  59. package/dest/sequencer/checkpoint_voter.js +106 -0
  60. package/dest/sequencer/config.d.ts +3 -2
  61. package/dest/sequencer/config.d.ts.map +1 -1
  62. package/dest/sequencer/events.d.ts +52 -0
  63. package/dest/sequencer/events.d.ts.map +1 -0
  64. package/dest/sequencer/events.js +1 -0
  65. package/dest/sequencer/index.d.ts +4 -2
  66. package/dest/sequencer/index.d.ts.map +1 -1
  67. package/dest/sequencer/index.js +3 -1
  68. package/dest/sequencer/metrics.d.ts +42 -7
  69. package/dest/sequencer/metrics.d.ts.map +1 -1
  70. package/dest/sequencer/metrics.js +241 -72
  71. package/dest/sequencer/sequencer.d.ts +146 -135
  72. package/dest/sequencer/sequencer.d.ts.map +1 -1
  73. package/dest/sequencer/sequencer.js +841 -661
  74. package/dest/sequencer/timetable.d.ts +67 -16
  75. package/dest/sequencer/timetable.d.ts.map +1 -1
  76. package/dest/sequencer/timetable.js +158 -64
  77. package/dest/sequencer/types.d.ts +3 -0
  78. package/dest/sequencer/types.d.ts.map +1 -0
  79. package/dest/sequencer/types.js +1 -0
  80. package/dest/sequencer/utils.d.ts +14 -8
  81. package/dest/sequencer/utils.d.ts.map +1 -1
  82. package/dest/sequencer/utils.js +7 -4
  83. package/dest/test/index.d.ts +6 -7
  84. package/dest/test/index.d.ts.map +1 -1
  85. package/dest/test/mock_checkpoint_builder.d.ts +95 -0
  86. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  87. package/dest/test/mock_checkpoint_builder.js +231 -0
  88. package/dest/test/utils.d.ts +53 -0
  89. package/dest/test/utils.d.ts.map +1 -0
  90. package/dest/test/utils.js +105 -0
  91. package/package.json +31 -30
  92. package/src/client/sequencer-client.ts +102 -62
  93. package/src/config.ts +132 -59
  94. package/src/global_variable_builder/README.md +44 -0
  95. package/src/global_variable_builder/fee_predictor.ts +172 -0
  96. package/src/global_variable_builder/fee_provider.ts +75 -0
  97. package/src/global_variable_builder/global_builder.ts +64 -96
  98. package/src/global_variable_builder/index.ts +3 -1
  99. package/src/index.ts +1 -7
  100. package/src/publisher/config.ts +166 -51
  101. package/src/publisher/index.ts +3 -0
  102. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  103. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  104. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  105. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  106. package/src/publisher/sequencer-publisher-factory.ts +43 -13
  107. package/src/publisher/sequencer-publisher-metrics.ts +19 -71
  108. package/src/publisher/sequencer-publisher.ts +763 -325
  109. package/src/sequencer/README.md +600 -0
  110. package/src/sequencer/chain_state_overrides.ts +87 -0
  111. package/src/sequencer/checkpoint_proposal_job.ts +1498 -0
  112. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  113. package/src/sequencer/checkpoint_voter.ts +119 -0
  114. package/src/sequencer/config.ts +2 -1
  115. package/src/sequencer/events.ts +32 -0
  116. package/src/sequencer/index.ts +3 -1
  117. package/src/sequencer/metrics.ts +302 -83
  118. package/src/sequencer/sequencer.ts +641 -863
  119. package/src/sequencer/timetable.ts +188 -81
  120. package/src/sequencer/types.ts +6 -0
  121. package/src/sequencer/utils.ts +18 -9
  122. package/src/test/index.ts +5 -6
  123. package/src/test/mock_checkpoint_builder.ts +323 -0
  124. package/src/test/utils.ts +185 -0
  125. package/dest/sequencer/block_builder.d.ts +0 -27
  126. package/dest/sequencer/block_builder.d.ts.map +0 -1
  127. package/dest/sequencer/block_builder.js +0 -134
  128. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  129. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  130. package/dest/tx_validator/nullifier_cache.js +0 -24
  131. package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
  132. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  133. package/dest/tx_validator/tx_validator_factory.js +0 -53
  134. package/src/sequencer/block_builder.ts +0 -222
  135. package/src/tx_validator/nullifier_cache.ts +0 -30
  136. package/src/tx_validator/tx_validator_factory.ts +0 -132
@@ -1,50 +1,82 @@
1
- import { L2Block } from '@aztec/aztec.js/block';
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
- import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
4
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
- type EmpireSlashingProposerContract,
7
- FormattedViemError,
6
+ FeeAssetPriceOracle,
8
7
  type GovernanceProposerContract,
9
8
  type IEmpireBase,
10
- type L1BlobInputs,
11
- type L1ContractsConfig,
12
- type L1TxConfig,
13
- type L1TxRequest,
14
9
  MULTI_CALL_3_ADDRESS,
15
10
  Multicall3,
16
11
  RollupContract,
17
- type TallySlashingProposerContract,
18
- type TransactionStats,
12
+ SimulationOverridesBuilder,
13
+ type SimulationOverridesPlan,
14
+ type SlashingProposerContract,
19
15
  type ViemCommitteeAttestations,
20
16
  type ViemHeader,
17
+ buildSimulationOverridesStateOverride,
18
+ } from '@aztec/ethereum/contracts';
19
+ import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
20
+ import {
21
+ type L1BlobInputs,
22
+ type L1TxConfig,
23
+ type L1TxRequest,
24
+ type L1TxUtils,
25
+ MAX_L1_TX_LIMIT,
26
+ type TransactionStats,
21
27
  WEI_CONST,
22
- formatViemError,
23
- tryExtractEvent,
24
- } from '@aztec/ethereum';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
28
+ } from '@aztec/ethereum/l1-tx-utils';
29
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
30
  import { sumBigint } from '@aztec/foundation/bigint';
27
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
33
+ import { pick } from '@aztec/foundation/collection';
34
+ import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
29
36
  import { EthAddress } from '@aztec/foundation/eth-address';
30
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
39
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
33
41
  import { bufferToHex } from '@aztec/foundation/string';
34
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
35
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
38
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
45
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
46
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
39
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
41
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
42
-
43
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
49
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
50
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
51
 
45
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import {
53
+ type Hex,
54
+ type TransactionReceipt,
55
+ type TypedDataDefinition,
56
+ encodeFunctionData,
57
+ keccak256,
58
+ multicall3Abi,
59
+ toHex,
60
+ } from 'viem';
61
+
62
+ import type { SequencerPublisherConfig } from './config.js';
63
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
46
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
47
65
 
66
+ /** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
67
+ export type SendRequestsResult = {
68
+ /** The L1 transaction receipt or error from the bundled multicall. */
69
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
70
+ /** Actions that expired (past their deadline) before the request was sent. */
71
+ expiredActions: Action[];
72
+ /** Actions that were included in the sent L1 transaction. */
73
+ sentActions: Action[];
74
+ /** Actions whose L1 simulation succeeded (subset of sentActions). */
75
+ successfulActions: Action[];
76
+ /** Actions whose L1 simulation failed (subset of sentActions). */
77
+ failedActions: Action[];
78
+ };
79
+
48
80
  /** Arguments to the process method of the rollup contract */
49
81
  type L1ProcessArgs = {
50
82
  /** The L2 block header. */
@@ -57,6 +89,8 @@ type L1ProcessArgs = {
57
89
  attestationsAndSigners: CommitteeAttestationsAndSigners;
58
90
  /** Attestations and signers signature */
59
91
  attestationsAndSignersSignature: Signature;
92
+ /** The fee asset price modifier in basis points (from oracle) */
93
+ feeAssetPriceModifier: bigint;
60
94
  };
61
95
 
62
96
  export const Actions = [
@@ -64,26 +98,30 @@ export const Actions = [
64
98
  'invalidate-by-insufficient-attestations',
65
99
  'propose',
66
100
  'governance-signal',
67
- 'empire-slashing-signal',
68
- 'create-empire-payload',
69
- 'execute-empire-payload',
70
101
  'vote-offenses',
71
102
  'execute-slash',
72
103
  ] as const;
73
104
 
74
105
  export type Action = (typeof Actions)[number];
75
106
 
76
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
77
108
 
78
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
79
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
80
111
 
81
- export type InvalidateBlockRequest = {
112
+ export type InvalidateCheckpointRequest = {
82
113
  request: L1TxRequest;
83
114
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
115
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
116
+ checkpointNumber: CheckpointNumber;
117
+ forcePendingCheckpointNumber: CheckpointNumber;
118
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
119
+ lastArchive: Fr;
120
+ };
121
+
122
+ type EnqueueProposeCheckpointOpts = {
123
+ txTimeoutAt?: Date;
124
+ simulationOverridesPlan?: SimulationOverridesPlan;
87
125
  };
88
126
 
89
127
  interface RequestWithExpiry {
@@ -92,6 +130,8 @@ interface RequestWithExpiry {
92
130
  lastValidL2Slot: SlotNumber;
93
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
94
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
95
135
  checkSuccess: (
96
136
  request: L1TxRequest,
97
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -102,23 +142,39 @@ export class SequencerPublisher {
102
142
  private interrupted = false;
103
143
  private metrics: SequencerPublisherMetrics;
104
144
  public epochCache: EpochCache;
145
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
105
146
 
106
147
  protected governanceLog = createLogger('sequencer:publisher:governance');
107
148
  protected slashingLog = createLogger('sequencer:publisher:slashing');
108
149
 
109
150
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
151
 
152
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
153
+ private payloadProposedCache: Set<string> = new Set<string>();
154
+
111
155
  protected log: Logger;
112
156
  protected ethereumSlotDuration: bigint;
157
+ protected aztecSlotDuration: bigint;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
113
161
 
114
- private blobSinkClient: BlobSinkClientInterface;
162
+ private blobClient: BlobClientInterface;
115
163
 
116
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
165
  private proposerAddressForSimulation?: EthAddress;
118
- // @note - with blobs, the below estimate seems too large.
119
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
120
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
121
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
166
+
167
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
168
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
169
+
170
+ /** L1 fee analyzer for fisherman mode */
171
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
172
+
173
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
174
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
175
+
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
122
178
 
123
179
  // A CALL to a cold address is 2700 gas
124
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -126,42 +182,48 @@ export class SequencerPublisher {
126
182
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
127
183
  public static VOTE_GAS_GUESS: bigint = 800_000n;
128
184
 
129
- public l1TxUtils: L1TxUtilsWithBlobs;
185
+ public l1TxUtils: L1TxUtils;
130
186
  public rollupContract: RollupContract;
131
187
  public govProposerContract: GovernanceProposerContract;
132
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
133
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
189
+
190
+ public readonly tracer: Tracer;
134
191
 
135
192
  protected requests: RequestWithExpiry[] = [];
136
193
 
137
194
  constructor(
138
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
195
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
196
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
139
197
  deps: {
140
198
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
142
- l1TxUtils: L1TxUtilsWithBlobs;
199
+ blobClient: BlobClientInterface;
200
+ l1TxUtils: L1TxUtils;
143
201
  rollupContract: RollupContract;
144
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
145
203
  governanceProposerContract: GovernanceProposerContract;
146
- slashFactoryContract: SlashFactoryContract;
147
204
  epochCache: EpochCache;
148
205
  dateProvider: DateProvider;
149
206
  metrics: SequencerPublisherMetrics;
150
207
  lastActions: Partial<Record<Action, SlotNumber>>;
151
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
152
210
  },
153
211
  ) {
154
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
155
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
156
216
  this.epochCache = deps.epochCache;
157
217
  this.lastActions = deps.lastActions;
158
218
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
219
+ this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
161
221
 
162
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
224
+ this.tracer = telemetry.getTracer('SequencerPublisher');
164
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
165
227
 
166
228
  this.rollupContract = deps.rollupContract;
167
229
 
@@ -173,17 +235,71 @@ export class SequencerPublisher {
173
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
174
236
  this.slashingProposerContract = newSlashingProposer;
175
237
  });
176
- this.slashFactoryContract = deps.slashFactoryContract;
238
+ // Initialize L1 fee analyzer for fisherman mode
239
+ if (config.fishermanMode) {
240
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(
241
+ this.l1TxUtils.client,
242
+ deps.dateProvider,
243
+ createLogger('sequencer:publisher:fee-analyzer'),
244
+ );
245
+ }
246
+
247
+ // Initialize fee asset price oracle
248
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
249
+ this.l1TxUtils.client,
250
+ this.rollupContract,
251
+ createLogger('sequencer:publisher:price-oracle'),
252
+ );
253
+
254
+ // Initialize failed L1 tx store (optional, for test networks)
255
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
256
+ }
257
+
258
+ /**
259
+ * Backs up a failed L1 transaction to the configured store for debugging.
260
+ * Does nothing if no store is configured.
261
+ */
262
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
263
+ if (!this.failedTxStore) {
264
+ return;
265
+ }
266
+
267
+ const tx: FailedL1Tx = {
268
+ ...failedTx,
269
+ timestamp: Date.now(),
270
+ };
271
+
272
+ // Fire and forget - don't block on backup
273
+ void this.failedTxStore
274
+ .then(store => store?.saveFailedTx(tx))
275
+ .catch(err => {
276
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
277
+ });
177
278
  }
178
279
 
179
280
  public getRollupContract(): RollupContract {
180
281
  return this.rollupContract;
181
282
  }
182
283
 
284
+ /**
285
+ * Gets the fee asset price modifier from the oracle.
286
+ * Returns 0n if the oracle query fails.
287
+ */
288
+ public getFeeAssetPriceModifier(): Promise<bigint> {
289
+ return this.feeAssetPriceOracle.computePriceModifier();
290
+ }
291
+
183
292
  public getSenderAddress() {
184
293
  return this.l1TxUtils.getSenderAddress();
185
294
  }
186
295
 
296
+ /**
297
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
298
+ */
299
+ public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
300
+ return this.l1FeeAnalyzer;
301
+ }
302
+
187
303
  /**
188
304
  * Sets the proposer address to use for simulations in fisherman mode.
189
305
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -197,7 +313,7 @@ export class SequencerPublisher {
197
313
  }
198
314
 
199
315
  public getCurrentL2Slot(): SlotNumber {
200
- return this.epochCache.getEpochAndSlotNow().slot;
316
+ return this.epochCache.getSlotNow();
201
317
  }
202
318
 
203
319
  /**
@@ -211,6 +327,62 @@ export class SequencerPublisher {
211
327
  }
212
328
  }
213
329
 
330
+ /**
331
+ * Analyzes L1 fees for the pending requests without sending them.
332
+ * This is used in fisherman mode to validate fee calculations.
333
+ * @param l2SlotNumber - The L2 slot number for this analysis
334
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
335
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
336
+ */
337
+ public async analyzeL1Fees(
338
+ l2SlotNumber: SlotNumber,
339
+ onComplete?: (analysis: L1FeeAnalysisResult) => void,
340
+ ): Promise<L1FeeAnalysisResult | undefined> {
341
+ if (!this.l1FeeAnalyzer) {
342
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
343
+ return undefined;
344
+ }
345
+
346
+ const requestsToAnalyze = [...this.requests];
347
+ if (requestsToAnalyze.length === 0) {
348
+ this.log.debug('No requests to analyze for L1 fees');
349
+ return undefined;
350
+ }
351
+
352
+ // Extract blob config from requests (if any)
353
+ const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
354
+ const blobConfig = blobConfigs[0];
355
+
356
+ // Get gas configs
357
+ const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
358
+ const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
359
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
360
+
361
+ // Get the transaction requests
362
+ const l1Requests = requestsToAnalyze.map(r => r.request);
363
+
364
+ // Start the analysis
365
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(
366
+ l2SlotNumber,
367
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
368
+ l1Requests,
369
+ blobConfig,
370
+ onComplete,
371
+ );
372
+
373
+ this.log.info('Started L1 fee analysis', {
374
+ analysisId,
375
+ l2SlotNumber: l2SlotNumber.toString(),
376
+ requestCount: requestsToAnalyze.length,
377
+ hasBlobConfig: !!blobConfig,
378
+ gasLimit: gasLimit.toString(),
379
+ actions: requestsToAnalyze.map(r => r.action),
380
+ });
381
+
382
+ // Return the analysis result (will be incomplete until block mines)
383
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
384
+ }
385
+
214
386
  /**
215
387
  * Sends all requests that are still valid.
216
388
  * @returns one of:
@@ -218,10 +390,12 @@ export class SequencerPublisher {
218
390
  * - a receipt and errorMsg if it failed on L1
219
391
  * - undefined if no valid requests are found OR the tx failed to send.
220
392
  */
221
- public async sendRequests() {
393
+ @trackSpan('SequencerPublisher.sendRequests')
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
222
395
  const requestsToProcess = [...this.requests];
223
396
  this.requests = [];
224
- if (this.interrupted) {
397
+
398
+ if (this.interrupted || requestsToProcess.length === 0) {
225
399
  return undefined;
226
400
  }
227
401
  const currentL2Slot = this.getCurrentL2Slot();
@@ -253,8 +427,8 @@ export class SequencerPublisher {
253
427
  // @note - we can only have one blob config per bundle
254
428
  // find requests with gas and blob configs
255
429
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
256
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
257
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
430
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
431
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
258
432
 
259
433
  if (blobConfigs.length > 1) {
260
434
  throw new Error('Multiple blob configs found');
@@ -264,7 +438,16 @@ export class SequencerPublisher {
264
438
 
265
439
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
266
440
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
267
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
441
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
442
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
443
+ const maxGas = MAX_L1_TX_LIMIT;
444
+ if (gasLimit !== undefined && gasLimit > maxGas) {
445
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
446
+ requested: gasLimit,
447
+ capped: maxGas,
448
+ });
449
+ gasLimit = maxGas;
450
+ }
268
451
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
269
452
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
270
453
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -274,19 +457,36 @@ export class SequencerPublisher {
274
457
  validRequests.sort((a, b) => compareActions(a.action, b.action));
275
458
 
276
459
  try {
460
+ // Capture context for failed tx backup before sending
461
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
462
+ const multicallData = encodeFunctionData({
463
+ abi: multicall3Abi,
464
+ functionName: 'aggregate3',
465
+ args: [
466
+ validRequests.map(r => ({
467
+ target: r.request.to!,
468
+ callData: r.request.data!,
469
+ allowFailure: true,
470
+ })),
471
+ ],
472
+ });
473
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
474
+
475
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
476
+
277
477
  this.log.debug('Forwarding transactions', {
278
478
  validRequests: validRequests.map(request => request.action),
279
479
  txConfig,
280
480
  });
281
- const result = await Multicall3.forward(
282
- validRequests.map(request => request.request),
283
- this.l1TxUtils,
284
- txConfig,
285
- blobConfig,
286
- this.rollupContract.address,
287
- this.log,
481
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
482
+ if (result === undefined) {
483
+ return undefined;
484
+ }
485
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
486
+ validRequests,
487
+ result,
488
+ txContext,
288
489
  );
289
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
290
490
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
291
491
  } catch (err) {
292
492
  const viemError = formatViemError(err);
@@ -304,16 +504,127 @@ export class SequencerPublisher {
304
504
  }
305
505
  }
306
506
 
507
+ /**
508
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
509
+ * failure occurs (i.e. the tx never reached the chain).
510
+ * On-chain reverts and simulation errors are returned as-is without rotation.
511
+ */
512
+ private async forwardWithPublisherRotation(
513
+ validRequests: RequestWithExpiry[],
514
+ txConfig: RequestWithExpiry['gasConfig'],
515
+ blobConfig: L1BlobInputs | undefined,
516
+ ) {
517
+ const triedAddresses: EthAddress[] = [];
518
+ let currentPublisher = this.l1TxUtils;
519
+
520
+ while (true) {
521
+ triedAddresses.push(currentPublisher.getSenderAddress());
522
+ try {
523
+ const result = await Multicall3.forward(
524
+ validRequests.map(r => r.request),
525
+ currentPublisher,
526
+ txConfig,
527
+ blobConfig,
528
+ this.rollupContract.address,
529
+ this.log,
530
+ );
531
+ this.l1TxUtils = currentPublisher;
532
+ return result;
533
+ } catch (err) {
534
+ if (err instanceof TimeoutError) {
535
+ throw err;
536
+ }
537
+ const viemError = formatViemError(err);
538
+ if (!this.getNextPublisher) {
539
+ this.log.error('Failed to publish bundled transactions', viemError);
540
+ return undefined;
541
+ }
542
+ this.log.warn(
543
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
544
+ viemError,
545
+ );
546
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
547
+ if (!nextPublisher) {
548
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
549
+ return undefined;
550
+ }
551
+ currentPublisher = nextPublisher;
552
+ }
553
+ }
554
+ }
555
+
556
+ /*
557
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
558
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
559
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
560
+ */
561
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
562
+ const ms = submitAfter.getTime() - this.dateProvider.now();
563
+ if (ms > 0) {
564
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
565
+ await this.interruptibleSleep.sleep(ms);
566
+ }
567
+ if (this.interrupted) {
568
+ return undefined;
569
+ }
570
+
571
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
572
+ const validRequests: RequestWithExpiry[] = [];
573
+ for (const request of this.requests) {
574
+ if (!request.preCheck) {
575
+ validRequests.push(request);
576
+ continue;
577
+ }
578
+
579
+ try {
580
+ await request.preCheck();
581
+ validRequests.push(request);
582
+ } catch (err) {
583
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
584
+ }
585
+ }
586
+
587
+ this.requests = validRequests;
588
+ if (this.requests.length === 0) {
589
+ return undefined;
590
+ }
591
+
592
+ return this.sendRequests();
593
+ }
594
+
307
595
  private callbackBundledTransactions(
308
596
  requests: RequestWithExpiry[],
309
- result?: { receipt: TransactionReceipt } | FormattedViemError,
597
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
598
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
310
599
  ) {
311
600
  const actionsListStr = requests.map(r => r.action).join(', ');
312
601
  if (result instanceof FormattedViemError) {
313
602
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
603
+ this.backupFailedTx({
604
+ id: keccak256(txContext.multicallData),
605
+ failureType: 'send-error',
606
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
607
+ blobData: txContext.blobData,
608
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
609
+ error: { message: result.message, name: result.name },
610
+ context: {
611
+ actions: requests.map(r => r.action),
612
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
613
+ sender: this.getSenderAddress().toString(),
614
+ },
615
+ });
314
616
  return { failedActions: requests.map(r => r.action) };
315
617
  } else {
316
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
618
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
619
+ result,
620
+ requests: requests.map(r => ({
621
+ ...r,
622
+ // Avoid logging large blob data
623
+ blobConfig: r.blobConfig
624
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
625
+ : undefined,
626
+ })),
627
+ });
317
628
  const successfulActions: Action[] = [];
318
629
  const failedActions: Action[] = [];
319
630
  for (const request of requests) {
@@ -323,27 +634,54 @@ export class SequencerPublisher {
323
634
  failedActions.push(request.action);
324
635
  }
325
636
  }
637
+ // Single backup for the whole reverted tx
638
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
639
+ this.backupFailedTx({
640
+ id: result.receipt.transactionHash,
641
+ failureType: 'revert',
642
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
643
+ blobData: txContext.blobData,
644
+ l1BlockNumber: result.receipt.blockNumber.toString(),
645
+ receipt: {
646
+ transactionHash: result.receipt.transactionHash,
647
+ blockNumber: result.receipt.blockNumber.toString(),
648
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
649
+ status: 'reverted',
650
+ },
651
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
652
+ context: {
653
+ actions: failedActions,
654
+ requests: requests
655
+ .filter(r => failedActions.includes(r.action))
656
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
657
+ sender: this.getSenderAddress().toString(),
658
+ },
659
+ });
660
+ }
326
661
  return { successfulActions, failedActions };
327
662
  }
328
663
  }
329
664
 
330
665
  /**
331
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
666
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
332
667
  * @param tipArchive - The archive to check
333
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
334
669
  */
335
- public canProposeAtNextEthBlock(
336
- tipArchive: Fr,
337
- msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
339
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
340
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
673
 
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
675
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
676
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
677
+
343
678
  return this.rollupContract
344
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
346
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
347
685
  .catch(err => {
348
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
349
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -355,27 +693,32 @@ export class SequencerPublisher {
355
693
  return undefined;
356
694
  });
357
695
  }
696
+
358
697
  /**
359
698
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
360
699
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
361
700
  * It will throw if the block header is invalid.
362
701
  * @param header - The block header to validate
363
702
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
703
+ @trackSpan('SequencerPublisher.validateBlockHeader')
704
+ public async validateBlockHeader(
705
+ header: CheckpointHeader,
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
707
+ ): Promise<void> {
365
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
709
 
367
710
  const args = [
368
711
  header.toViem(),
369
- CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
712
+ CommitteeAttestationsAndSigners.packAttestations([]),
370
713
  [], // no signers
371
714
  Signature.empty().toViemSignature(),
372
715
  `0x${'0'.repeat(64)}`, // 32 empty bytes
373
- header.contentCommitment.blobsHash.toString(),
716
+ header.blobsHash.toString(),
374
717
  flags,
375
718
  ] as const;
376
719
 
377
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
379
722
  let balance = 0n;
380
723
  if (this.config.fishermanMode) {
381
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -395,99 +738,130 @@ export class SequencerPublisher {
395
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
396
739
  from: MULTI_CALL_3_ADDRESS,
397
740
  },
398
- { time: ts + 1n },
741
+ { time: ts },
399
742
  stateOverrides,
400
743
  );
401
744
  this.log.debug(`Simulated validateHeader`);
402
745
  }
403
746
 
404
747
  /**
405
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
406
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
748
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
749
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
407
750
  */
408
- public async simulateInvalidateBlock(
409
- validationResult: ValidateBlockResult,
410
- ): Promise<InvalidateBlockRequest | undefined> {
751
+ public async simulateInvalidateCheckpoint(
752
+ validationResult: ValidateCheckpointResult,
753
+ ): Promise<InvalidateCheckpointRequest | undefined> {
411
754
  if (validationResult.valid) {
412
755
  return undefined;
413
756
  }
414
757
 
415
- const { reason, block } = validationResult;
416
- const blockNumber = block.blockNumber;
417
- const logData = { ...block, reason };
758
+ const { reason, checkpoint } = validationResult;
759
+ const checkpointNumber = checkpoint.checkpointNumber;
760
+ const logData = { ...checkpoint, reason };
418
761
 
419
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
420
- if (currentBlockNumber < validationResult.block.blockNumber) {
762
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
763
+ if (currentCheckpointNumber < checkpointNumber) {
421
764
  this.log.verbose(
422
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
423
- { currentBlockNumber, ...logData },
765
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
766
+ { currentCheckpointNumber, ...logData },
424
767
  );
425
768
  return undefined;
426
769
  }
427
770
 
428
- const request = this.buildInvalidateBlockRequest(validationResult);
429
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
771
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
772
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
773
+
774
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
430
775
 
431
776
  try {
432
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
433
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
777
+ const { gasUsed } = await this.l1TxUtils.simulate(
778
+ request,
779
+ undefined,
780
+ undefined,
781
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
782
+ );
783
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
784
+ ...logData,
785
+ request,
786
+ gasUsed,
787
+ });
434
788
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
789
+ return {
790
+ request,
791
+ gasUsed,
792
+ checkpointNumber,
793
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
795
+ reason,
796
+ };
436
797
  } catch (err) {
437
798
  const viemError = formatViemError(err);
438
799
 
439
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
440
- // we can safely ignore it and return undefined so we go ahead with block building.
441
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
800
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
801
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
802
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
442
803
  this.log.verbose(
443
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
804
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
444
805
  { ...logData, request, error: viemError.message },
445
806
  );
446
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
447
- if (latestPendingBlockNumber < blockNumber) {
448
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
809
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
449
810
  return undefined;
450
811
  } else {
451
812
  this.log.error(
452
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
813
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
453
814
  viemError,
454
815
  logData,
455
816
  );
456
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
457
- cause: viemError,
458
- });
817
+ throw new Error(
818
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
819
+ {
820
+ cause: viemError,
821
+ },
822
+ );
459
823
  }
460
824
  }
461
825
 
462
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
463
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
464
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
826
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
827
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
828
+ this.backupFailedTx({
829
+ id: keccak256(request.data!),
830
+ failureType: 'simulation',
831
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
832
+ l1BlockNumber: l1BlockNumber.toString(),
833
+ error: { message: viemError.message, name: viemError.name },
834
+ context: {
835
+ actions: [`invalidate-${reason}`],
836
+ checkpointNumber,
837
+ sender: this.getSenderAddress().toString(),
838
+ },
839
+ });
840
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
465
841
  }
466
842
  }
467
843
 
468
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
844
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
469
845
  if (validationResult.valid) {
470
- throw new Error('Cannot invalidate a valid block');
846
+ throw new Error('Cannot invalidate a valid checkpoint');
471
847
  }
472
848
 
473
- const { block, committee, reason } = validationResult;
474
- const logData = { ...block, reason };
475
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
849
+ const { checkpoint, committee, reason } = validationResult;
850
+ const logData = { ...checkpoint, reason };
851
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
476
852
 
477
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(
478
- validationResult.attestations,
479
- ).getPackedAttestations();
853
+ const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
480
854
 
481
855
  if (reason === 'invalid-attestation') {
482
856
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
857
+ checkpoint.checkpointNumber,
484
858
  attestationsAndSigners,
485
859
  committee,
486
860
  validationResult.invalidIndex,
487
861
  );
488
862
  } else if (reason === 'insufficient-attestations') {
489
863
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
864
+ checkpoint.checkpointNumber,
491
865
  attestationsAndSigners,
492
866
  committee,
493
867
  );
@@ -497,47 +871,24 @@ export class SequencerPublisher {
497
871
  }
498
872
  }
499
873
 
500
- /**
501
- * @notice Will simulate `propose` to make sure that the block is valid for submission
502
- *
503
- * @dev Throws if unable to propose
504
- *
505
- * @param block - The block to propose
506
- * @param attestationData - The block's attestation data
507
- *
508
- */
509
- public async validateBlockForSubmission(
510
- block: L2Block,
874
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
875
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
876
+ public async validateCheckpointForSubmission(
877
+ checkpoint: Checkpoint,
511
878
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
879
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
514
- ): Promise<bigint> {
515
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
516
-
517
- // If we have no attestations, we still need to provide the empty attestations
518
- // so that the committee is recalculated correctly
519
- const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
520
- if (ignoreSignatures) {
521
- const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
522
- if (!committee) {
523
- this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
524
- throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
525
- }
526
- attestationsAndSigners.attestations = committee.map(committeeMember =>
527
- CommitteeAttestation.fromAddress(committeeMember),
528
- );
529
- }
530
-
531
- const blobFields = block.getCheckpointBlobFields();
532
- const blobs = getBlobsPerL1Block(blobFields);
880
+ simulationOverridesPlan?: SimulationOverridesPlan,
881
+ ): Promise<void> {
882
+ const blobFields = checkpoint.toBlobFields();
883
+ const blobs = await getBlobsPerL1Block(blobFields);
533
884
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
885
 
535
886
  const args = [
536
887
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
888
+ header: checkpoint.header.toViem(),
889
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
890
  oracleInput: {
540
- feeAssetPriceModifier: 0n,
891
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
541
892
  },
542
893
  },
543
894
  attestationsAndSigners.getPackedAttestations(),
@@ -546,13 +897,11 @@ export class SequencerPublisher {
546
897
  blobInput,
547
898
  ] as const;
548
899
 
549
- await this.simulateProposeTx(args, ts, options);
550
- return ts;
900
+ await this.simulateProposeTx(args, simulationOverridesPlan);
551
901
  }
552
902
 
553
903
  private async enqueueCastSignalHelper(
554
904
  slotNumber: SlotNumber,
555
- timestamp: bigint,
556
905
  signalType: GovernanceSignalAction,
557
906
  payload: EthAddress,
558
907
  base: IEmpireBase,
@@ -573,10 +922,45 @@ export class SequencerPublisher {
573
922
  const round = await base.computeRound(slotNumber);
574
923
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
924
 
925
+ if (roundInfo.quorumReached) {
926
+ return false;
927
+ }
928
+
576
929
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
930
  return false;
578
931
  }
579
932
 
933
+ if (await this.isPayloadEmpty(payload)) {
934
+ this.log.warn(`Skipping vote cast for payload with empty code`);
935
+ return false;
936
+ }
937
+
938
+ // Check if payload was already submitted to governance
939
+ const cacheKey = payload.toString();
940
+ if (!this.payloadProposedCache.has(cacheKey)) {
941
+ try {
942
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
943
+ const proposed = await retry(
944
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
945
+ 'Check if payload was proposed',
946
+ makeBackoff([0, 1, 2]),
947
+ this.log,
948
+ true,
949
+ );
950
+ if (proposed) {
951
+ this.payloadProposedCache.add(cacheKey);
952
+ }
953
+ } catch (err) {
954
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
955
+ return false;
956
+ }
957
+ }
958
+
959
+ if (this.payloadProposedCache.has(cacheKey)) {
960
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
961
+ return false;
962
+ }
963
+
580
964
  const cachedLastVote = this.lastActions[signalType];
581
965
  this.lastActions[signalType] = slotNumber;
582
966
  const action = signalType;
@@ -595,11 +979,30 @@ export class SequencerPublisher {
595
979
  lastValidL2Slot: slotNumber,
596
980
  });
597
981
 
982
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
983
+ const timestamp = this.getSimulationTimestamp(slotNumber);
984
+
598
985
  try {
599
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
986
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
600
987
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
601
988
  } catch (err) {
602
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
989
+ const viemError = formatViemError(err);
990
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
991
+ simulationTimestamp: timestamp,
992
+ l1BlockNumber,
993
+ });
994
+ this.backupFailedTx({
995
+ id: keccak256(request.data!),
996
+ failureType: 'simulation',
997
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
998
+ l1BlockNumber: l1BlockNumber.toString(),
999
+ error: { message: viemError.message, name: viemError.name },
1000
+ context: {
1001
+ actions: [action],
1002
+ slot: slotNumber,
1003
+ sender: this.getSenderAddress().toString(),
1004
+ },
1005
+ });
603
1006
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
604
1007
  }
605
1008
 
@@ -619,14 +1022,14 @@ export class SequencerPublisher {
619
1022
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
1023
  if (!success) {
621
1024
  this.log.error(
622
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
1025
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
623
1026
  logData,
624
1027
  );
625
1028
  this.lastActions[signalType] = cachedLastVote;
626
1029
  return false;
627
1030
  } else {
628
1031
  this.log.info(
629
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
1032
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
630
1033
  logData,
631
1034
  );
632
1035
  return true;
@@ -636,22 +1039,30 @@ export class SequencerPublisher {
636
1039
  return true;
637
1040
  }
638
1041
 
1042
+ private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
1043
+ const key = payload.toString();
1044
+ const cached = this.isPayloadEmptyCache.get(key);
1045
+ if (cached) {
1046
+ return cached;
1047
+ }
1048
+ const isEmpty = !(await this.l1TxUtils.getCode(payload));
1049
+ this.isPayloadEmptyCache.set(key, isEmpty);
1050
+ return isEmpty;
1051
+ }
1052
+
639
1053
  /**
640
1054
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
1055
  * @param slotNumber - The slot number to cast a signal for.
642
- * @param timestamp - The timestamp of the slot to cast a signal for.
643
1056
  * @returns True if the signal was successfully enqueued, false otherwise.
644
1057
  */
645
1058
  public enqueueGovernanceCastSignal(
646
1059
  governancePayload: EthAddress,
647
1060
  slotNumber: SlotNumber,
648
- timestamp: bigint,
649
1061
  signerAddress: EthAddress,
650
1062
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
651
1063
  ): Promise<boolean> {
652
1064
  return this.enqueueCastSignalHelper(
653
1065
  slotNumber,
654
- timestamp,
655
1066
  'governance-signal',
656
1067
  governancePayload,
657
1068
  this.govProposerContract,
@@ -664,7 +1075,6 @@ export class SequencerPublisher {
664
1075
  public async enqueueSlashingActions(
665
1076
  actions: ProposerSlashAction[],
666
1077
  slotNumber: SlotNumber,
667
- timestamp: bigint,
668
1078
  signerAddress: EthAddress,
669
1079
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
670
1080
  ): Promise<boolean> {
@@ -675,58 +1085,6 @@ export class SequencerPublisher {
675
1085
 
676
1086
  for (const action of actions) {
677
1087
  switch (action.type) {
678
- case 'vote-empire-payload': {
679
- if (this.slashingProposerContract?.type !== 'empire') {
680
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
681
- break;
682
- }
683
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
684
- signerAddress,
685
- });
686
- await this.enqueueCastSignalHelper(
687
- slotNumber,
688
- timestamp,
689
- 'empire-slashing-signal',
690
- action.payload,
691
- this.slashingProposerContract,
692
- signerAddress,
693
- signer,
694
- );
695
- break;
696
- }
697
-
698
- case 'create-empire-payload': {
699
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
700
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
701
- await this.simulateAndEnqueueRequest(
702
- 'create-empire-payload',
703
- request,
704
- (receipt: TransactionReceipt) =>
705
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
706
- slotNumber,
707
- timestamp,
708
- );
709
- break;
710
- }
711
-
712
- case 'execute-empire-payload': {
713
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
714
- if (this.slashingProposerContract?.type !== 'empire') {
715
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
716
- return false;
717
- }
718
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
719
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
720
- await this.simulateAndEnqueueRequest(
721
- 'execute-empire-payload',
722
- request,
723
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
724
- slotNumber,
725
- timestamp,
726
- );
727
- break;
728
- }
729
-
730
1088
  case 'vote-offenses': {
731
1089
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
732
1090
  slotNumber,
@@ -734,19 +1092,17 @@ export class SequencerPublisher {
734
1092
  votesCount: action.votes.length,
735
1093
  signerAddress,
736
1094
  });
737
- if (this.slashingProposerContract?.type !== 'tally') {
738
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1095
+ if (!this.slashingProposerContract) {
1096
+ this.log.error('No slashing proposer contract available');
739
1097
  return false;
740
1098
  }
741
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
742
1099
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
743
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1100
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
744
1101
  await this.simulateAndEnqueueRequest(
745
1102
  'vote-offenses',
746
1103
  request,
747
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1104
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
748
1105
  slotNumber,
749
- timestamp,
750
1106
  );
751
1107
  break;
752
1108
  }
@@ -757,18 +1113,20 @@ export class SequencerPublisher {
757
1113
  round: action.round,
758
1114
  signerAddress,
759
1115
  });
760
- if (this.slashingProposerContract?.type !== 'tally') {
761
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1116
+ if (!this.slashingProposerContract) {
1117
+ this.log.error('No slashing proposer contract available');
762
1118
  return false;
763
1119
  }
764
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
765
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1120
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1121
+ action.round,
1122
+ action.committees,
1123
+ );
766
1124
  await this.simulateAndEnqueueRequest(
767
1125
  'execute-slash',
768
- request,
769
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1126
+ executeRequest,
1127
+ (receipt: TransactionReceipt) =>
1128
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
770
1129
  slotNumber,
771
- timestamp,
772
1130
  );
773
1131
  break;
774
1132
  }
@@ -783,33 +1141,30 @@ export class SequencerPublisher {
783
1141
  return true;
784
1142
  }
785
1143
 
786
- /**
787
- * Proposes a L2 block on L1.
788
- *
789
- * @param block - L2 block to propose.
790
- * @returns True if the tx has been enqueued, throws otherwise. See #9315
791
- */
792
- public async enqueueProposeL2Block(
793
- block: L2Block,
1144
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
1145
+ public async enqueueProposeCheckpoint(
1146
+ checkpoint: Checkpoint,
794
1147
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
1148
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
1149
+ opts: EnqueueProposeCheckpointOpts = {},
1150
+ ): Promise<void> {
1151
+ const checkpointHeader = checkpoint.header;
799
1152
 
800
- const blobFields = block.getCheckpointBlobFields();
801
- const blobs = getBlobsPerL1Block(blobFields);
1153
+ const blobFields = checkpoint.toBlobFields();
1154
+ const blobs = await getBlobsPerL1Block(blobFields);
802
1155
 
803
- const proposeTxArgs = {
1156
+ const proposeTxArgs: L1ProcessArgs = {
804
1157
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
1158
+ archive: checkpoint.archive.root.toBuffer(),
807
1159
  blobs,
808
1160
  attestationsAndSigners,
809
1161
  attestationsAndSignersSignature,
1162
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
810
1163
  };
811
1164
 
812
- let ts: bigint;
1165
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1166
+ .withoutBlobCheck()
1167
+ .build();
813
1168
 
814
1169
  try {
815
1170
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -817,22 +1172,53 @@ export class SequencerPublisher {
817
1172
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
1173
  // make time consistency checks break.
819
1174
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
820
- ts = await this.validateBlockForSubmission(block, attestationsAndSigners, attestationsAndSignersSignature, opts);
1175
+ await this.validateCheckpointForSubmission(
1176
+ checkpoint,
1177
+ attestationsAndSigners,
1178
+ attestationsAndSignersSignature,
1179
+ simulationOverridesPlan,
1180
+ );
821
1181
  } catch (err: any) {
822
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
823
- ...block.getStats(),
824
- slotNumber: block.header.globalVariables.slotNumber,
825
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
1182
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1183
+ ...checkpoint.getStats(),
1184
+ slotNumber: checkpoint.header.slotNumber,
1185
+ simulationOverridesPlan,
826
1186
  });
827
1187
  throw err;
828
1188
  }
829
1189
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
1190
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1191
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1192
+ let preCheck = undefined;
1193
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1194
+ preCheck = async () => {
1195
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1196
+ await this.validateCheckpointForSubmission(
1197
+ checkpoint,
1198
+ attestationsAndSigners,
1199
+ attestationsAndSignersSignature,
1200
+ simulationOverridesPlan,
1201
+ );
1202
+ };
1203
+ }
1204
+
1205
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1206
+ ...checkpoint.toCheckpointInfo(),
1207
+ txTimeoutAt: opts.txTimeoutAt,
1208
+ simulationOverridesPlan,
1209
+ });
1210
+ await this.addProposeTx(
1211
+ checkpoint,
1212
+ proposeTxArgs,
1213
+ { txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
1214
+ preCheck,
1215
+ );
833
1216
  }
834
1217
 
835
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
1218
+ public enqueueInvalidateCheckpoint(
1219
+ request: InvalidateCheckpointRequest | undefined,
1220
+ opts: { txTimeoutAt?: Date } = {},
1221
+ ) {
836
1222
  if (!request) {
837
1223
  return;
838
1224
  }
@@ -840,9 +1226,9 @@ export class SequencerPublisher {
840
1226
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
841
1227
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
842
1228
 
843
- const { gasUsed, blockNumber } = request;
844
- const logData = { gasUsed, blockNumber, gasLimit, opts };
845
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1229
+ const { gasUsed, checkpointNumber } = request;
1230
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
1231
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
846
1232
  this.addRequest({
847
1233
  action: `invalidate-by-${request.reason}`,
848
1234
  request: request.request,
@@ -855,9 +1241,9 @@ export class SequencerPublisher {
855
1241
  result.receipt.status === 'success' &&
856
1242
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
857
1243
  if (!success) {
858
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1244
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
859
1245
  } else {
860
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1246
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
861
1247
  }
862
1248
  return !!success;
863
1249
  },
@@ -869,8 +1255,8 @@ export class SequencerPublisher {
869
1255
  request: L1TxRequest,
870
1256
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
871
1257
  slotNumber: SlotNumber,
872
- timestamp: bigint,
873
1258
  ) {
1259
+ const timestamp = this.getSimulationTimestamp(slotNumber);
874
1260
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
875
1261
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
876
1262
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -882,13 +1268,31 @@ export class SequencerPublisher {
882
1268
 
883
1269
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
884
1270
 
1271
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1272
+
885
1273
  let gasUsed: bigint;
1274
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1275
+
886
1276
  try {
887
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1277
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
888
1278
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
889
1279
  } catch (err) {
890
- const viemError = formatViemError(err);
1280
+ const viemError = formatViemError(err, simulateAbi);
891
1281
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1282
+
1283
+ this.backupFailedTx({
1284
+ id: keccak256(request.data!),
1285
+ failureType: 'simulation',
1286
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1287
+ l1BlockNumber: l1BlockNumber.toString(),
1288
+ error: { message: viemError.message, name: viemError.name },
1289
+ context: {
1290
+ actions: [action],
1291
+ slot: slotNumber,
1292
+ sender: this.getSenderAddress().toString(),
1293
+ },
1294
+ });
1295
+
892
1296
  return false;
893
1297
  }
894
1298
 
@@ -896,10 +1300,14 @@ export class SequencerPublisher {
896
1300
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
897
1301
  logData.gasLimit = gasLimit;
898
1302
 
1303
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1304
+ // when the tx is sent and a revert is diagnosed via simulation.
1305
+ const requestWithAbi = { ...request, abi: simulateAbi };
1306
+
899
1307
  this.log.debug(`Enqueuing ${action}`, logData);
900
1308
  this.addRequest({
901
1309
  action,
902
- request,
1310
+ request: requestWithAbi,
903
1311
  gasConfig: { gasLimit },
904
1312
  lastValidL2Slot: slotNumber,
905
1313
  checkSuccess: (_req, result) => {
@@ -924,6 +1332,7 @@ export class SequencerPublisher {
924
1332
  */
925
1333
  public interrupt() {
926
1334
  this.interrupted = true;
1335
+ this.interruptibleSleep.interrupt();
927
1336
  this.l1TxUtils.interrupt();
928
1337
  }
929
1338
 
@@ -933,11 +1342,7 @@ export class SequencerPublisher {
933
1342
  this.l1TxUtils.restart();
934
1343
  }
935
1344
 
936
- private async prepareProposeTx(
937
- encodedData: L1ProcessArgs,
938
- timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
940
- ) {
1345
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
941
1346
  const kzg = Blob.getViemKzgInstance();
942
1347
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
943
1348
  this.log.debug('Validating blob input', { blobInput });
@@ -968,9 +1373,27 @@ export class SequencerPublisher {
968
1373
  kzg,
969
1374
  },
970
1375
  )
971
- .catch(err => {
972
- const { message, metaMessages } = formatViemError(err);
973
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1376
+ .catch(async err => {
1377
+ const viemError = formatViemError(err);
1378
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1379
+ const validateBlobsData = encodeFunctionData({
1380
+ abi: RollupAbi,
1381
+ functionName: 'validateBlobs',
1382
+ args: [blobInput],
1383
+ });
1384
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1385
+ this.backupFailedTx({
1386
+ id: keccak256(validateBlobsData),
1387
+ failureType: 'simulation',
1388
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1389
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1390
+ l1BlockNumber: l1BlockNumber.toString(),
1391
+ error: { message: viemError.message, name: viemError.name },
1392
+ context: {
1393
+ actions: ['validate-blobs'],
1394
+ sender: this.getSenderAddress().toString(),
1395
+ },
1396
+ });
974
1397
  throw new Error('Failed to validate blobs');
975
1398
  });
976
1399
  }
@@ -981,8 +1404,7 @@ export class SequencerPublisher {
981
1404
  header: encodedData.header.toViem(),
982
1405
  archive: toHex(encodedData.archive),
983
1406
  oracleInput: {
984
- // We are currently not modifying these. See #9963
985
- feeAssetPriceModifier: 0n,
1407
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
986
1408
  },
987
1409
  },
988
1410
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -991,7 +1413,7 @@ export class SequencerPublisher {
991
1413
  blobInput,
992
1414
  ] as const;
993
1415
 
994
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1416
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
995
1417
 
996
1418
  return { args, blobEvaluationGas, rollupData, simulationResult };
997
1419
  }
@@ -999,7 +1421,6 @@ export class SequencerPublisher {
999
1421
  /**
1000
1422
  * Simulates the propose tx with eth_simulateV1
1001
1423
  * @param args - The propose tx args
1002
- * @param timestamp - The timestamp to simulate proposal at
1003
1424
  * @returns The simulation result
1004
1425
  */
1005
1426
  private async simulateProposeTx(
@@ -1008,7 +1429,7 @@ export class SequencerPublisher {
1008
1429
  readonly header: ViemHeader;
1009
1430
  readonly archive: `0x${string}`;
1010
1431
  readonly oracleInput: {
1011
- readonly feeAssetPriceModifier: 0n;
1432
+ readonly feeAssetPriceModifier: bigint;
1012
1433
  };
1013
1434
  },
1014
1435
  ViemCommitteeAttestations,
@@ -1016,8 +1437,7 @@ export class SequencerPublisher {
1016
1437
  ViemSignature,
1017
1438
  `0x${string}`,
1018
1439
  ],
1019
- timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1440
+ simulationOverridesPlan?: SimulationOverridesPlan,
1021
1441
  ) {
1022
1442
  const rollupData = encodeFunctionData({
1023
1443
  abi: RollupAbi,
@@ -1025,23 +1445,7 @@ export class SequencerPublisher {
1025
1445
  args,
1026
1446
  });
1027
1447
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1030
- options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1032
- : []
1033
- ).flatMap(override => override.stateDiff ?? []);
1034
-
1035
- const stateOverrides: StateOverride = [
1036
- {
1037
- address: this.rollupContract.address,
1038
- // @note we override checkBlob to false since blobs are not part simulate()
1039
- stateDiff: [
1040
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1042
- ],
1043
- },
1044
- ];
1448
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1045
1449
  // In fisherman mode, simulate as the proposer but with sufficient balance
1046
1450
  if (this.proposerAddressForSimulation) {
1047
1451
  stateOverrides.push({
@@ -1050,25 +1454,27 @@ export class SequencerPublisher {
1050
1454
  });
1051
1455
  }
1052
1456
 
1457
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1458
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1459
+
1053
1460
  const simulationResult = await this.l1TxUtils
1054
1461
  .simulate(
1055
1462
  {
1056
1463
  to: this.rollupContract.address,
1057
1464
  data: rollupData,
1058
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1465
+ gas: MAX_L1_TX_LIMIT,
1059
1466
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1060
1467
  },
1061
1468
  {
1062
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1063
- time: timestamp + 1n,
1469
+ time: simTs,
1064
1470
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1065
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1471
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1066
1472
  },
1067
1473
  stateOverrides,
1068
1474
  RollupAbi,
1069
1475
  {
1070
1476
  // @note fallback gas estimate to use if the node doesn't support simulation API
1071
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1477
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1072
1478
  },
1073
1479
  )
1074
1480
  .catch(err => {
@@ -1078,11 +1484,23 @@ export class SequencerPublisher {
1078
1484
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1079
1485
  // Return a minimal simulation result with the fallback gas estimate
1080
1486
  return {
1081
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1487
+ gasUsed: MAX_L1_TX_LIMIT,
1082
1488
  logs: [],
1083
1489
  };
1084
1490
  }
1085
- this.log.error(`Failed to simulate propose tx`, viemError);
1491
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1492
+ this.backupFailedTx({
1493
+ id: keccak256(rollupData),
1494
+ failureType: 'simulation',
1495
+ request: { to: this.rollupContract.address, data: rollupData },
1496
+ l1BlockNumber: l1BlockNumber.toString(),
1497
+ error: { message: viemError.message, name: viemError.name },
1498
+ context: {
1499
+ actions: ['propose'],
1500
+ slot: Number(args[0].header.slotNumber),
1501
+ sender: this.getSenderAddress().toString(),
1502
+ },
1503
+ });
1086
1504
  throw err;
1087
1505
  });
1088
1506
 
@@ -1090,17 +1508,17 @@ export class SequencerPublisher {
1090
1508
  }
1091
1509
 
1092
1510
  private async addProposeTx(
1093
- block: L2Block,
1511
+ checkpoint: Checkpoint,
1094
1512
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1096
- timestamp: bigint,
1513
+ opts: EnqueueProposeCheckpointOpts = {},
1514
+ preCheck?: () => Promise<void>,
1097
1515
  ): Promise<void> {
1516
+ const slot = checkpoint.header.slotNumber;
1098
1517
  const timer = new Timer();
1099
1518
  const kzg = Blob.getViemKzgInstance();
1100
1519
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1101
1520
  encodedData,
1102
- timestamp,
1103
- opts,
1521
+ opts.simulationOverridesPlan,
1104
1522
  );
1105
1523
  const startBlock = await this.l1TxUtils.getBlockNumber();
1106
1524
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1109,11 +1527,13 @@ export class SequencerPublisher {
1109
1527
  SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1110
1528
  );
1111
1529
 
1112
- // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1113
- // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
1114
- void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch(_err => {
1115
- this.log.error('Failed to send blobs to blob sink');
1116
- });
1530
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1531
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1532
+ void Promise.resolve().then(() =>
1533
+ this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
1534
+ this.log.error('Failed to send blobs to blob client');
1535
+ }),
1536
+ );
1117
1537
 
1118
1538
  return this.addRequest({
1119
1539
  action: 'propose',
@@ -1121,8 +1541,9 @@ export class SequencerPublisher {
1121
1541
  to: this.rollupContract.address,
1122
1542
  data: rollupData,
1123
1543
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1125
- gasConfig: { ...opts, gasLimit },
1544
+ lastValidL2Slot: checkpoint.header.slotNumber,
1545
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1546
+ preCheck,
1126
1547
  blobConfig: {
1127
1548
  blobs: encodedData.blobs.map(b => b.data),
1128
1549
  kzg,
@@ -1136,11 +1557,12 @@ export class SequencerPublisher {
1136
1557
  receipt &&
1137
1558
  receipt.status === 'success' &&
1138
1559
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1560
+
1139
1561
  if (success) {
1140
1562
  const endBlock = receipt.blockNumber;
1141
1563
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1564
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1565
+ const publishStats: L1PublishCheckpointStats = {
1144
1566
  gasPrice: receipt.effectiveGasPrice,
1145
1567
  gasUsed: receipt.gasUsed,
1146
1568
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,26 +1571,42 @@ export class SequencerPublisher {
1149
1571
  calldataGas,
1150
1572
  calldataSize,
1151
1573
  sender,
1152
- ...block.getStats(),
1574
+ ...checkpoint.getStats(),
1153
1575
  eventName: 'rollup-published-to-l1',
1154
1576
  blobCount: encodedData.blobs.length,
1155
1577
  inclusionBlocks,
1156
1578
  };
1157
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
1579
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
1580
+ ...stats,
1581
+ ...checkpoint.getStats(),
1582
+ ...pick(receipt, 'transactionHash', 'blockHash'),
1583
+ });
1158
1584
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1585
 
1160
1586
  return true;
1161
1587
  } else {
1162
1588
  this.metrics.recordFailedTx('process');
1163
- this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
1164
- ...block.getStats(),
1165
- receipt,
1166
- txHash: receipt.transactionHash,
1167
- slotNumber: block.header.globalVariables.slotNumber,
1168
- });
1589
+ this.log.error(
1590
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1591
+ undefined,
1592
+ { ...checkpoint.getStats(), ...receipt },
1593
+ );
1169
1594
  return false;
1170
1595
  }
1171
1596
  },
1172
1597
  });
1173
1598
  }
1599
+
1600
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1601
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1602
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1603
+ const l1Constants = this.epochCache.getL1Constants();
1604
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1605
+ }
1606
+
1607
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1608
+ private getNextL1SlotTimestamp(): bigint {
1609
+ const l1Constants = this.epochCache.getL1Constants();
1610
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1611
+ }
1174
1612
  }