@aztec/sequencer-client 0.0.1-commit.3fd054f6 → 0.0.1-commit.431c48d

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 (113) hide show
  1. package/README.md +283 -21
  2. package/dest/client/sequencer-client.d.ts +20 -5
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +26 -22
  5. package/dest/config.d.ts +13 -4
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +67 -26
  8. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  10. package/dest/global_variable_builder/fee_predictor.js +138 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  13. package/dest/global_variable_builder/fee_provider.js +80 -0
  14. package/dest/global_variable_builder/global_builder.d.ts +6 -25
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +7 -65
  17. package/dest/global_variable_builder/index.d.ts +3 -1
  18. package/dest/global_variable_builder/index.d.ts.map +1 -1
  19. package/dest/global_variable_builder/index.js +2 -0
  20. package/dest/publisher/config.d.ts +7 -3
  21. package/dest/publisher/config.d.ts.map +1 -1
  22. package/dest/publisher/config.js +13 -3
  23. package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
  24. package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
  25. package/dest/publisher/l1_to_l2_messaging.js +70 -0
  26. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  28. package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
  29. package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
  30. package/dest/publisher/sequencer-bundle-simulator.js +198 -0
  31. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  32. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  33. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  34. package/dest/publisher/sequencer-publisher.d.ts +88 -71
  35. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  36. package/dest/publisher/sequencer-publisher.js +434 -548
  37. package/dest/publisher/write_json.d.ts +11 -0
  38. package/dest/publisher/write_json.d.ts.map +1 -0
  39. package/dest/publisher/write_json.js +57 -0
  40. package/dest/sequencer/automine/automine_factory.d.ts +56 -0
  41. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
  42. package/dest/sequencer/automine/automine_factory.js +85 -0
  43. package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
  44. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
  45. package/dest/sequencer/automine/automine_sequencer.js +696 -0
  46. package/dest/sequencer/automine/index.d.ts +3 -0
  47. package/dest/sequencer/automine/index.d.ts.map +1 -0
  48. package/dest/sequencer/automine/index.js +2 -0
  49. package/dest/sequencer/checkpoint_proposal_job.d.ts +71 -21
  50. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  51. package/dest/sequencer/checkpoint_proposal_job.js +738 -214
  52. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  53. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  54. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  55. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  56. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  57. package/dest/sequencer/checkpoint_voter.js +2 -5
  58. package/dest/sequencer/errors.d.ts +1 -8
  59. package/dest/sequencer/errors.d.ts.map +1 -1
  60. package/dest/sequencer/errors.js +0 -9
  61. package/dest/sequencer/events.d.ts +61 -5
  62. package/dest/sequencer/events.d.ts.map +1 -1
  63. package/dest/sequencer/metrics.d.ts +10 -11
  64. package/dest/sequencer/metrics.d.ts.map +1 -1
  65. package/dest/sequencer/metrics.js +34 -20
  66. package/dest/sequencer/requests_tracker.d.ts +22 -0
  67. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  68. package/dest/sequencer/requests_tracker.js +33 -0
  69. package/dest/sequencer/sequencer.d.ts +157 -33
  70. package/dest/sequencer/sequencer.d.ts.map +1 -1
  71. package/dest/sequencer/sequencer.js +566 -158
  72. package/dest/sequencer/types.d.ts +2 -2
  73. package/dest/sequencer/types.d.ts.map +1 -1
  74. package/dest/test/index.d.ts +3 -3
  75. package/dest/test/index.d.ts.map +1 -1
  76. package/dest/test/utils.d.ts +15 -1
  77. package/dest/test/utils.d.ts.map +1 -1
  78. package/dest/test/utils.js +25 -7
  79. package/package.json +28 -27
  80. package/src/client/sequencer-client.ts +37 -27
  81. package/src/config.ts +79 -26
  82. package/src/global_variable_builder/README.md +44 -0
  83. package/src/global_variable_builder/fee_predictor.ts +182 -0
  84. package/src/global_variable_builder/fee_provider.ts +97 -0
  85. package/src/global_variable_builder/global_builder.ts +12 -80
  86. package/src/global_variable_builder/index.ts +2 -0
  87. package/src/publisher/config.ts +30 -7
  88. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  89. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  90. package/src/publisher/sequencer-bundle-simulator.ts +254 -0
  91. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  92. package/src/publisher/sequencer-publisher.ts +494 -596
  93. package/src/publisher/write_json.ts +78 -0
  94. package/src/sequencer/automine/README.md +60 -0
  95. package/src/sequencer/automine/automine_factory.ts +152 -0
  96. package/src/sequencer/automine/automine_sequencer.ts +800 -0
  97. package/src/sequencer/automine/index.ts +6 -0
  98. package/src/sequencer/checkpoint_proposal_job.ts +854 -243
  99. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  100. package/src/sequencer/checkpoint_voter.ts +1 -12
  101. package/src/sequencer/errors.ts +0 -15
  102. package/src/sequencer/events.ts +66 -5
  103. package/src/sequencer/metrics.ts +49 -25
  104. package/src/sequencer/requests_tracker.ts +43 -0
  105. package/src/sequencer/sequencer.ts +646 -175
  106. package/src/sequencer/types.ts +1 -1
  107. package/src/test/index.ts +2 -2
  108. package/src/test/utils.ts +61 -10
  109. package/dest/sequencer/timetable.d.ts +0 -88
  110. package/dest/sequencer/timetable.d.ts.map +0 -1
  111. package/dest/sequencer/timetable.js +0 -222
  112. package/src/sequencer/README.md +0 -531
  113. package/src/sequencer/timetable.ts +0 -283
@@ -1,23 +1,23 @@
1
- import { RollupContract } from '@aztec/ethereum/contracts';
2
- import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
1
+ import {
2
+ RollupContract,
3
+ type SimulationOverridesPlan,
4
+ buildSimulationOverridesStateOverride,
5
+ } from '@aztec/ethereum/contracts';
3
6
  import type { ViemPublicClient } from '@aztec/ethereum/types';
4
- import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
7
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
5
8
  import { Fr } from '@aztec/foundation/curves/bn254';
6
9
  import type { EthAddress } from '@aztec/foundation/eth-address';
7
- import { createLogger } from '@aztec/foundation/log';
8
- import type { DateProvider } from '@aztec/foundation/timer';
9
10
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
10
- import { type L1RollupConstants, getNextL1SlotTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
11
+ import { type L1RollupConstants, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
11
12
  import { GasFees } from '@aztec/stdlib/gas';
12
13
  import type {
13
14
  CheckpointGlobalVariables,
14
15
  GlobalVariableBuilder as GlobalVariableBuilderInterface,
15
16
  } from '@aztec/stdlib/tx';
16
- import { GlobalVariables } from '@aztec/stdlib/tx';
17
17
 
18
18
  /** Configuration for the GlobalVariableBuilder (excludes L1 client config). */
19
19
  export type GlobalVariableBuilderConfig = {
20
- l1Contracts: Pick<L1ContractAddresses, 'rollupAddress'>;
20
+ rollupAddress: EthAddress;
21
21
  ethereumSlotDuration: number;
22
22
  rollupVersion: bigint;
23
23
  } & Pick<L1RollupConstants, 'slotDuration' | 'l1GenesisTime'>;
@@ -26,12 +26,7 @@ export type GlobalVariableBuilderConfig = {
26
26
  * Simple global variables builder.
27
27
  */
28
28
  export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
29
- private log = createLogger('sequencer:global_variable_builder');
30
- private currentMinFees: Promise<GasFees> = Promise.resolve(new GasFees(0, 0));
31
- private currentL1BlockNumber: bigint | undefined = undefined;
32
-
33
29
  private readonly rollupContract: RollupContract;
34
- private readonly ethereumSlotDuration: number;
35
30
  private readonly aztecSlotDuration: number;
36
31
  private readonly l1GenesisTime: bigint;
37
32
 
@@ -39,79 +34,16 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
39
34
  private version: Fr;
40
35
 
41
36
  constructor(
42
- private readonly dateProvider: DateProvider,
43
37
  private readonly publicClient: ViemPublicClient,
44
38
  config: GlobalVariableBuilderConfig,
45
39
  ) {
46
40
  this.version = new Fr(config.rollupVersion);
47
41
  this.chainId = new Fr(this.publicClient.chain!.id);
48
42
 
49
- this.ethereumSlotDuration = config.ethereumSlotDuration;
50
43
  this.aztecSlotDuration = config.slotDuration;
51
44
  this.l1GenesisTime = config.l1GenesisTime;
52
45
 
53
- this.rollupContract = new RollupContract(this.publicClient, config.l1Contracts.rollupAddress);
54
- }
55
-
56
- /**
57
- * Computes the "current" min fees, e.g., the price that you currently should pay to get include in the next block
58
- * @returns Min fees for the next block
59
- */
60
- private async computeCurrentMinFees(): Promise<GasFees> {
61
- // Since this might be called in the middle of a slot where a block might have been published,
62
- // we need to fetch the last block written, and estimate the earliest timestamp for the next block.
63
- // The timestamp of that last block will act as a lower bound for the next block.
64
-
65
- const lastCheckpoint = await this.rollupContract.getPendingCheckpoint();
66
- const earliestTimestamp = await this.rollupContract.getTimestampForSlot(
67
- SlotNumber.fromBigInt(BigInt(lastCheckpoint.slotNumber) + 1n),
68
- );
69
- const nextEthTimestamp = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), {
70
- l1GenesisTime: this.l1GenesisTime,
71
- ethereumSlotDuration: this.ethereumSlotDuration,
72
- });
73
- const timestamp = earliestTimestamp > nextEthTimestamp ? earliestTimestamp : nextEthTimestamp;
74
-
75
- return new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true));
76
- }
77
-
78
- public async getCurrentMinFees(): Promise<GasFees> {
79
- // Get the current block number
80
- const blockNumber = await this.publicClient.getBlockNumber();
81
-
82
- // If the L1 block number has changed then chain a new promise to get the current min fees
83
- if (this.currentL1BlockNumber === undefined || blockNumber > this.currentL1BlockNumber) {
84
- this.currentL1BlockNumber = blockNumber;
85
- this.currentMinFees = this.currentMinFees.then(() => this.computeCurrentMinFees());
86
- }
87
- return this.currentMinFees;
88
- }
89
-
90
- /**
91
- * Simple builder of global variables.
92
- * @param blockNumber - The block number to build global variables for.
93
- * @param coinbase - The address to receive block reward.
94
- * @param feeRecipient - The address to receive fees.
95
- * @param slotNumber - The slot number to use for the global variables, if undefined it will be calculated.
96
- * @returns The global variables for the given block number.
97
- */
98
- public async buildGlobalVariables(
99
- blockNumber: BlockNumber,
100
- coinbase: EthAddress,
101
- feeRecipient: AztecAddress,
102
- maybeSlot?: SlotNumber,
103
- ): Promise<GlobalVariables> {
104
- const slot: SlotNumber =
105
- maybeSlot ??
106
- (await this.rollupContract.getSlotAt(
107
- getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), {
108
- l1GenesisTime: this.l1GenesisTime,
109
- ethereumSlotDuration: this.ethereumSlotDuration,
110
- }),
111
- ));
112
-
113
- const checkpointGlobalVariables = await this.buildCheckpointGlobalVariables(coinbase, feeRecipient, slot);
114
- return GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables });
46
+ this.rollupContract = new RollupContract(this.publicClient, config.rollupAddress);
115
47
  }
116
48
 
117
49
  /** Builds global variables that are constant throughout a checkpoint. */
@@ -119,6 +51,7 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
119
51
  coinbase: EthAddress,
120
52
  feeRecipient: AztecAddress,
121
53
  slotNumber: SlotNumber,
54
+ simulationOverridesPlan?: SimulationOverridesPlan,
122
55
  ): Promise<CheckpointGlobalVariables> {
123
56
  const { chainId, version } = this;
124
57
 
@@ -127,9 +60,8 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
127
60
  l1GenesisTime: this.l1GenesisTime,
128
61
  });
129
62
 
130
- // We can skip much of the logic in getCurrentMinFees since it we already check that we are not within a slot elsewhere.
131
- // TODO(palla/mbps): Can we use a cached value here?
132
- const gasFees = new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true));
63
+ const stateOverride = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
64
+ const gasFees = new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true, stateOverride));
133
65
 
134
66
  return { chainId, version, slotNumber, timestamp, coinbase, feeRecipient, gasFees };
135
67
  }
@@ -1 +1,3 @@
1
+ export { FeeProviderImpl } from './fee_provider.js';
1
2
  export { GlobalVariableBuilder, type GlobalVariableBuilderConfig } from './global_builder.js';
3
+ export { FeePredictor } from './fee_predictor.js';
@@ -1,7 +1,12 @@
1
1
  import { type BlobClientConfig, blobClientConfigMapping } from '@aztec/blob-client/client/config';
2
2
  import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum/l1-reader';
3
3
  import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from '@aztec/ethereum/l1-tx-utils/config';
4
- import { type ConfigMappingsType, SecretValue, booleanConfigHelper } from '@aztec/foundation/config';
4
+ import {
5
+ type ConfigMappingsType,
6
+ SecretValue,
7
+ booleanConfigHelper,
8
+ numberConfigHelper,
9
+ } from '@aztec/foundation/config';
5
10
  import { EthAddress } from '@aztec/foundation/eth-address';
6
11
 
7
12
  import { parseEther } from 'viem';
@@ -90,6 +95,10 @@ export type SequencerPublisherConfig = L1TxUtilsConfig &
90
95
  fishermanMode?: boolean;
91
96
  sequencerPublisherAllowInvalidStates?: boolean;
92
97
  sequencerPublisherForwarderAddress?: EthAddress;
98
+ /** How long to wait for the previous L1 block before sending scheduled publisher txs anyway. */
99
+ sequencerPublisherPreviousL1BlockWaitTimeoutMs: number;
100
+ /** Poll interval while waiting for the previous L1 block before scheduled publisher txs. */
101
+ sequencerPublisherPreviousL1BlockWaitPollIntervalMs: number;
93
102
  /** Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path */
94
103
  l1TxFailedStore?: string;
95
104
  /** Min ETH balance below which a publisher gets funded. Undefined = funding disabled. */
@@ -115,12 +124,13 @@ export function getPublisherConfigFromSequencerConfig(config: SequencerPublisher
115
124
  };
116
125
  }
117
126
 
118
- export const proverTxSenderConfigMappings: ConfigMappingsType<Omit<ProverTxSenderConfig, 'l1Contracts'>> = {
127
+ export const proverTxSenderConfigMappings: ConfigMappingsType<ProverTxSenderConfig> = {
119
128
  ...l1ReaderConfigMappings,
120
129
  proverPublisherPrivateKeys: {
121
130
  env: `PROVER_PUBLISHER_PRIVATE_KEYS`,
122
131
  description: 'The private keys to be used by the prover publisher.',
123
- parseEnv: (val: string) => val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}`)),
132
+ parseEnv: (val: string) =>
133
+ val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}` as `0x${string}`)),
124
134
  defaultValue: [],
125
135
  fallback: [`PROVER_PUBLISHER_PRIVATE_KEY`],
126
136
  },
@@ -132,12 +142,13 @@ export const proverTxSenderConfigMappings: ConfigMappingsType<Omit<ProverTxSende
132
142
  },
133
143
  };
134
144
 
135
- export const sequencerTxSenderConfigMappings: ConfigMappingsType<Omit<SequencerTxSenderConfig, 'l1Contracts'>> = {
145
+ export const sequencerTxSenderConfigMappings: ConfigMappingsType<SequencerTxSenderConfig> = {
136
146
  ...l1ReaderConfigMappings,
137
147
  sequencerPublisherPrivateKeys: {
138
148
  env: `SEQ_PUBLISHER_PRIVATE_KEYS`,
139
149
  description: 'The private keys to be used by the sequencer publisher.',
140
- parseEnv: (val: string) => val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}`)),
150
+ parseEnv: (val: string) =>
151
+ val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}` as `0x${string}`)),
141
152
  defaultValue: [],
142
153
  fallback: [`SEQ_PUBLISHER_PRIVATE_KEY`],
143
154
  },
@@ -166,7 +177,19 @@ export const sequencerPublisherConfigMappings: ConfigMappingsType<SequencerPubli
166
177
  sequencerPublisherForwarderAddress: {
167
178
  env: `SEQ_PUBLISHER_FORWARDER_ADDRESS`,
168
179
  description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
169
- parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
180
+ parseEnv: (val: string) => EthAddress.fromString(val),
181
+ },
182
+ sequencerPublisherPreviousL1BlockWaitTimeoutMs: {
183
+ env: `SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_TIMEOUT_MS`,
184
+ description:
185
+ 'How long to wait for the previous L1 block before sending scheduled publisher txs anyway, in milliseconds.',
186
+ ...numberConfigHelper(8_000),
187
+ },
188
+ sequencerPublisherPreviousL1BlockWaitPollIntervalMs: {
189
+ env: `SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_POLL_INTERVAL_MS`,
190
+ description:
191
+ 'Poll interval while waiting for the previous L1 block before scheduled publisher txs, in milliseconds.',
192
+ ...numberConfigHelper(500),
170
193
  },
171
194
  l1TxFailedStore: {
172
195
  env: 'L1_TX_FAILED_STORE',
@@ -192,7 +215,7 @@ export const proverPublisherConfigMappings: ConfigMappingsType<ProverPublisherCo
192
215
  proverPublisherForwarderAddress: {
193
216
  env: `PROVER_PUBLISHER_FORWARDER_ADDRESS`,
194
217
  description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
195
- parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
218
+ parseEnv: (val: string) => EthAddress.fromString(val),
196
219
  },
197
220
  ...publisherFundingConfigMappings,
198
221
  };
@@ -0,0 +1,85 @@
1
+ import { RollupContract } from '@aztec/ethereum/contracts';
2
+ import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
3
+ import type { ExtendedViemWalletClient } from '@aztec/ethereum/types';
4
+ import { Fr } from '@aztec/foundation/curves/bn254';
5
+ import { tryJsonStringify } from '@aztec/foundation/json-rpc';
6
+ import { createLogger } from '@aztec/foundation/log';
7
+ import { InboxAbi } from '@aztec/l1-artifacts';
8
+ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
9
+
10
+ import { decodeEventLog, getContract } from 'viem';
11
+
12
+ /**
13
+ * Sends an L1-to-L2 message via the Inbox contract and returns its hash and global leaf index.
14
+ * Test helper copied from the end-to-end fixtures so this integration test can live in sequencer-client.
15
+ */
16
+ export async function sendL1ToL2Message(
17
+ message: { recipient: AztecAddress; content: Fr; secretHash: Fr },
18
+ ctx: {
19
+ l1Client: ExtendedViemWalletClient;
20
+ l1ContractAddresses: Pick<L1ContractAddresses, 'inboxAddress' | 'rollupAddress'>;
21
+ },
22
+ ) {
23
+ const logger = createLogger('sequencer-client:l1_to_l2_messaging');
24
+ const inbox = getContract({
25
+ address: ctx.l1ContractAddresses.inboxAddress.toString(),
26
+ abi: InboxAbi,
27
+ client: ctx.l1Client,
28
+ });
29
+
30
+ const { recipient, content, secretHash } = message;
31
+
32
+ const version = await new RollupContract(ctx.l1Client, ctx.l1ContractAddresses.rollupAddress.toString()).getVersion();
33
+
34
+ // We inject the message to Inbox
35
+ const txHash = await inbox.write.sendL2Message(
36
+ [{ actor: recipient.toString(), version: BigInt(version) }, content.toString(), secretHash.toString()],
37
+ {
38
+ gas: 1_000_000n,
39
+ },
40
+ );
41
+ logger.info(`L1 to L2 message sent in tx ${txHash}`);
42
+
43
+ // We check that the message was correctly injected by checking the emitted event
44
+ const txReceipt = await ctx.l1Client.waitForTransactionReceipt({ hash: txHash });
45
+
46
+ if (txReceipt.status !== 'success') {
47
+ throw new Error(`L1 to L2 message failed to be sent in tx ${txHash}. Status: ${txReceipt.status}`);
48
+ }
49
+
50
+ logger.info(`L1 to L2 message receipt retrieved for tx ${txReceipt.transactionHash}`, txReceipt);
51
+
52
+ if (txReceipt.transactionHash !== txHash) {
53
+ throw new Error(`Receipt transaction hash mismatch: ${txReceipt.transactionHash} !== ${txHash}`);
54
+ }
55
+
56
+ // Filter for MessageSent events from the Inbox contract by trying to decode each log
57
+ const messageSentLogs = txReceipt.logs
58
+ .filter(log => log.address.toLowerCase() === ctx.l1ContractAddresses.inboxAddress.toString().toLowerCase())
59
+ .map(log => {
60
+ try {
61
+ const decoded = decodeEventLog({
62
+ abi: InboxAbi,
63
+ data: log.data,
64
+ topics: log.topics,
65
+ });
66
+ return { log, decoded };
67
+ } catch {
68
+ return null; // Not a decodable event from this ABI
69
+ }
70
+ })
71
+ .filter((item): item is { log: any; decoded: any } => item !== null && item.decoded.eventName === 'MessageSent');
72
+
73
+ if (messageSentLogs.length !== 1) {
74
+ throw new Error(
75
+ `Wrong number of MessageSent logs found in ${txHash} transaction (got ${messageSentLogs.length} expected 1)\n${tryJsonStringify(messageSentLogs.map(item => item.log))}`,
76
+ );
77
+ }
78
+
79
+ // We already have the decoded event
80
+ const topics = messageSentLogs[0].decoded;
81
+ const receivedMsgHash = topics.args.hash;
82
+ const receivedGlobalLeafIndex = topics.args.index;
83
+
84
+ return { msgHash: Fr.fromHexString(receivedMsgHash), globalLeafIndex: new Fr(receivedGlobalLeafIndex), txReceipt };
85
+ }
@@ -1,7 +1,9 @@
1
+ import type { Branded } from '@aztec/foundation/branded-types';
2
+
1
3
  import type { Hex } from 'viem';
2
4
 
3
5
  /** URI pointing to a stored failed L1 transaction. */
4
- export type FailedL1TxUri = string & { __brand: 'FailedL1TxUri' };
6
+ export type FailedL1TxUri = Branded<string, 'FailedL1TxUri'>;
5
7
 
6
8
  /** A failed L1 transaction captured for debugging and replay. */
7
9
  export type FailedL1Tx = {
@@ -0,0 +1,254 @@
1
+ import type { EpochCache } from '@aztec/epoch-cache';
2
+ import { Multicall3, type RollupContract, buildSimulationOverridesStateOverride } from '@aztec/ethereum/contracts';
3
+ import { type L1TxUtils, MAX_L1_TX_LIMIT } from '@aztec/ethereum/l1-tx-utils';
4
+ import { formatViemError } from '@aztec/ethereum/utils';
5
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
6
+ import { type Logger, createLogger } from '@aztec/foundation/log';
7
+ import { getLastL1SlotTimestampForL2Slot } from '@aztec/stdlib/epoch-helpers';
8
+
9
+ import type { Hex, StateOverride } from 'viem';
10
+
11
+ import type { RequestWithExpiry } from './sequencer-publisher.js';
12
+
13
+ /** A request that was dropped by bundle simulation, with the decoded revert reason. */
14
+ export type DroppedRequest = {
15
+ request: RequestWithExpiry;
16
+ revertReason: string | undefined;
17
+ returnData: Hex | undefined;
18
+ };
19
+
20
+ /**
21
+ * Result of {@link SequencerBundleSimulator.simulate}.
22
+ *
23
+ * - `success`: simulation succeeded. `requests` is the filtered survivor list, `gasLimit` is
24
+ * the bumped gas limit derived from `gasUsed` (plus blob evaluation gas). `droppedRequests`
25
+ * lists the entries that were observed to revert in simulation.
26
+ * - `fallback`: the node does not support eth_simulateV1 (or the simulate call threw). The
27
+ * caller should send `requests` as-is with a safe gas limit (e.g. {@link MAX_L1_TX_LIMIT}).
28
+ * `droppedRequests` carries any entries that the first pass already proved reverted, so the
29
+ * caller does not re-include them when the second pass falls back.
30
+ * - `aborted`: the bundle cannot be sent. `droppedRequests` contains only entries that were
31
+ * actually observed to revert (so they can be reported as simulation failures); it is empty
32
+ * when the abort was caused by an empty input bundle.
33
+ */
34
+ export type BundleSimulateResult =
35
+ | { kind: 'success'; requests: RequestWithExpiry[]; gasLimit: bigint; droppedRequests: DroppedRequest[] }
36
+ | { kind: 'fallback'; requests: RequestWithExpiry[]; droppedRequests: DroppedRequest[] }
37
+ | { kind: 'aborted'; reason: AbortReason; droppedRequests: DroppedRequest[] };
38
+
39
+ export type AbortReason = 'empty-bundle' | 'all-reverted' | 'second-pass-reverts';
40
+
41
+ type SimulatePassResult =
42
+ | { kind: 'decoded'; survivors: RequestWithExpiry[]; droppedRequests: DroppedRequest[]; gasUsed: bigint }
43
+ | { kind: 'fallback' };
44
+
45
+ /**
46
+ * Bundle-level simulator for the aggregate3 payload that `SequencerPublisher` is about to send.
47
+ *
48
+ * Runs `eth_simulateV1` against `Multicall3.aggregate3`, drops entries that revert, and returns
49
+ * a gasLimit for the survivors. When `eth_simulateV1` is unavailable, signals fallback to the
50
+ * caller so it can send the bundle as-is with a conservative gas limit.
51
+ */
52
+ export class SequencerBundleSimulator {
53
+ private readonly log: Logger;
54
+
55
+ constructor(
56
+ private readonly deps: {
57
+ getL1TxUtils: () => L1TxUtils;
58
+ rollupContract: RollupContract;
59
+ epochCache: EpochCache;
60
+ log?: Logger;
61
+ },
62
+ ) {
63
+ this.log = deps.log ?? createLogger('sequencer:publisher:bundle-simulator');
64
+ }
65
+
66
+ /**
67
+ * Simulates the given bundle near the end of the target slot and filters out entries
68
+ * that revert.
69
+ *
70
+ * - If all entries pass on the first pass, returns `success` with the gasLimit.
71
+ * - If some entries revert, re-simulates the survivors. If the second pass is clean, returns
72
+ * `success` with the survivors and dropped entries. If the second pass surfaces any revert,
73
+ * returns `aborted` — we refuse to send a bundle whose composition still has internal
74
+ * reverts after one round of filtering.
75
+ * - If eth_simulateV1 is unavailable, returns `fallback`. The caller is expected to send the
76
+ * bundle as-is with a safe gas limit.
77
+ *
78
+ * The simulation `block.timestamp` is the last L1 slot timestamp inside the target L2 slot.
79
+ * This still maps to the target L2 slot for propose's `validateHeader` and EIP-712 signature
80
+ * checks, while avoiding eth_simulateV1 rejecting a child block whose timestamp is not strictly
81
+ * greater than the current L1 head.
82
+ *
83
+ * Known limitation: on networks where L1 is mining behind cadence (missed L1 slots, anvil with
84
+ * overridden timestamps), the actual `block.timestamp` at send time can land in the prior L2
85
+ * slot. In that case `propose` would revert silently inside the multicall. The simulator does
86
+ * not detect this case because it simulates inside the target slot — the prior implementation
87
+ * used `min(predictedNextL1Ts, targetTimestamp)` to surface this failure mode at simulate time.
88
+ */
89
+ public async simulate(validRequests: RequestWithExpiry[], targetSlot: SlotNumber): Promise<BundleSimulateResult> {
90
+ if (validRequests.length === 0) {
91
+ return { kind: 'aborted', reason: 'empty-bundle', droppedRequests: [] };
92
+ }
93
+ // Pin the publisher we'll use across the whole simulate call so that the publisher's rotation
94
+ // can't change l1TxUtils mid-flight.
95
+ const l1TxUtils = this.deps.getL1TxUtils();
96
+
97
+ const proposeRequest = validRequests.find(r => r.action === 'propose');
98
+ const simulateTimestamp = getLastL1SlotTimestampForL2Slot(targetSlot, this.deps.epochCache.getL1Constants());
99
+ const firstPassOverrides = await this.buildStateOverrides(!!proposeRequest);
100
+
101
+ const firstPass = await this.simulateAndDecode(l1TxUtils, validRequests, simulateTimestamp, firstPassOverrides);
102
+
103
+ if (firstPass.kind === 'fallback') {
104
+ this.log.warn('Bundle simulate fallback (eth_simulateV1 unavailable); caller will send bundle as-is', {
105
+ actions: validRequests.map(r => r.action),
106
+ });
107
+ return { kind: 'fallback', requests: validRequests, droppedRequests: [] };
108
+ }
109
+
110
+ if (firstPass.survivors.length === 0) {
111
+ this.log.warn('All bundle entries dropped in simulation; aborting send', {
112
+ actions: validRequests.map(r => r.action),
113
+ });
114
+ return { kind: 'aborted', reason: 'all-reverted', droppedRequests: firstPass.droppedRequests };
115
+ }
116
+
117
+ if (firstPass.droppedRequests.length === 0) {
118
+ return this.buildSuccessResult(l1TxUtils, firstPass.survivors, [], firstPass.gasUsed, proposeRequest);
119
+ }
120
+
121
+ this.log.warn('Some bundle entries reverted; re-simulating reduced bundle', {
122
+ droppedActions: firstPass.droppedRequests.map(d => d.request.action),
123
+ remainingActions: firstPass.survivors.map(r => r.action),
124
+ });
125
+
126
+ // Rebuild overrides for the reduced bundle: if propose was dropped, we no longer need the blob-check override
127
+ const proposeSurvived = proposeRequest !== undefined && firstPass.survivors.includes(proposeRequest);
128
+ const secondPassOverrides = proposeSurvived ? firstPassOverrides : await this.buildStateOverrides(false);
129
+ const secondPass = await this.simulateAndDecode(
130
+ l1TxUtils,
131
+ firstPass.survivors,
132
+ simulateTimestamp,
133
+ secondPassOverrides,
134
+ );
135
+
136
+ if (secondPass.kind === 'fallback') {
137
+ this.log.warn(
138
+ 'Bundle simulate errored on second pass (eth_simulateV1 unavailable); sending first-pass survivors as-is',
139
+ {
140
+ actions: firstPass.survivors.map(r => r.action),
141
+ droppedActions: firstPass.droppedRequests.map(d => d.request.action),
142
+ },
143
+ );
144
+ return { kind: 'fallback', requests: firstPass.survivors, droppedRequests: firstPass.droppedRequests };
145
+ }
146
+
147
+ // We refuse to chase reverts through repeated trimming: anything other than a clean second pass aborts the whole send
148
+ if (secondPass.droppedRequests.length > 0) {
149
+ this.log.error('Re-simulate surfaced reverts; aborting send', {
150
+ secondPassDroppedActions: secondPass.droppedRequests.map(d => d.request.action),
151
+ });
152
+ return {
153
+ kind: 'aborted',
154
+ reason: 'second-pass-reverts',
155
+ droppedRequests: [...firstPass.droppedRequests, ...secondPass.droppedRequests],
156
+ };
157
+ }
158
+
159
+ return this.buildSuccessResult(
160
+ l1TxUtils,
161
+ secondPass.survivors,
162
+ firstPass.droppedRequests,
163
+ secondPass.gasUsed,
164
+ proposeRequest,
165
+ );
166
+ }
167
+
168
+ private buildSuccessResult(
169
+ l1TxUtils: L1TxUtils,
170
+ survivors: RequestWithExpiry[],
171
+ droppedRequests: DroppedRequest[],
172
+ bundleGasUsed: bigint,
173
+ proposeRequest: RequestWithExpiry | undefined,
174
+ ): BundleSimulateResult {
175
+ const proposeSurvived = proposeRequest !== undefined && survivors.includes(proposeRequest);
176
+ const blobEvaluationGas = proposeSurvived ? (proposeRequest?.blobEvaluationGas ?? 0n) : 0n;
177
+ const gasLimit = this.computeGasLimit(l1TxUtils, bundleGasUsed, blobEvaluationGas);
178
+ this.log.debug('Bundle simulate complete', {
179
+ survivingRequests: survivors.length,
180
+ bundleGasUsed,
181
+ gasLimit,
182
+ actions: survivors.map(r => r.action),
183
+ });
184
+ return { kind: 'success', requests: survivors, gasLimit, droppedRequests };
185
+ }
186
+
187
+ /**
188
+ * `gasLimit = bumpGasLimit(ceil(gasUsed * 64 / 63))`, plus blob evaluation gas if a propose
189
+ * survived, capped at the L1 block gas limit.
190
+ */
191
+ private computeGasLimit(l1TxUtils: L1TxUtils, bundleGasUsed: bigint, blobEvaluationGas: bigint): bigint {
192
+ const gasUsedWithEip150 = (bundleGasUsed * 64n + 62n) / 63n;
193
+ const gasLimit = l1TxUtils.bumpGasLimit(gasUsedWithEip150) + blobEvaluationGas;
194
+ return gasLimit > MAX_L1_TX_LIMIT ? MAX_L1_TX_LIMIT : gasLimit;
195
+ }
196
+
197
+ /**
198
+ * eth_simulateV1 cannot carry blob sidecar data, so disable the rollup's on-chain blob check
199
+ * when a propose is in the bundle.
200
+ */
201
+ private buildStateOverrides(hasProposeAction: boolean): Promise<StateOverride> {
202
+ return buildSimulationOverridesStateOverride(
203
+ this.deps.rollupContract,
204
+ hasProposeAction ? { disableBlobCheck: true } : undefined,
205
+ );
206
+ }
207
+
208
+ private async simulateAndDecode(
209
+ l1TxUtils: L1TxUtils,
210
+ requests: RequestWithExpiry[],
211
+ simulateTimestamp: bigint,
212
+ stateOverrides: StateOverride,
213
+ ): Promise<SimulatePassResult> {
214
+ let simResult: Awaited<ReturnType<typeof Multicall3.simulateAggregate3>>;
215
+ try {
216
+ simResult = await Multicall3.simulateAggregate3(
217
+ requests.map(r => ({ to: r.request.to! as Hex, data: r.request.data! as Hex, abi: r.request.abi })),
218
+ l1TxUtils,
219
+ {
220
+ blockOverrides: { time: simulateTimestamp, gasLimit: MAX_L1_TX_LIMIT * 2n },
221
+ stateOverrides,
222
+ gas: MAX_L1_TX_LIMIT,
223
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
224
+ },
225
+ );
226
+ } catch (err) {
227
+ this.log.warn('Bundle simulate threw; treating as fallback', {
228
+ err: formatViemError(err),
229
+ actions: requests.map(r => r.action),
230
+ });
231
+ return { kind: 'fallback' };
232
+ }
233
+
234
+ if (simResult.kind === 'fallback') {
235
+ return { kind: 'fallback' };
236
+ }
237
+
238
+ const survivors: RequestWithExpiry[] = [];
239
+ const droppedRequests: DroppedRequest[] = [];
240
+ for (let i = 0; i < requests.length; i++) {
241
+ const entry = simResult.entries[i];
242
+ if (entry.success) {
243
+ survivors.push(requests[i]);
244
+ continue;
245
+ }
246
+ droppedRequests.push({
247
+ request: requests[i],
248
+ revertReason: entry.revertReason,
249
+ returnData: entry.returnData,
250
+ });
251
+ }
252
+ return { kind: 'decoded', survivors, droppedRequests, gasUsed: simResult.gasUsed };
253
+ }
254
+ }
@@ -7,7 +7,6 @@ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
7
7
  import type { PublisherFilter, PublisherManager } from '@aztec/ethereum/publisher-manager';
8
8
  import { SlotNumber } from '@aztec/foundation/branded-types';
9
9
  import type { DateProvider } from '@aztec/foundation/timer';
10
- import type { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
11
10
  import type { TelemetryClient } from '@aztec/telemetry-client';
12
11
  import { NodeKeystoreAdapter } from '@aztec/validator-client';
13
12
 
@@ -40,7 +39,6 @@ export class SequencerPublisherFactory {
40
39
  epochCache: EpochCache;
41
40
  rollupContract: RollupContract;
42
41
  governanceProposerContract: GovernanceProposerContract;
43
- slashFactoryContract: SlashFactoryContract;
44
42
  nodeKeyStore: NodeKeystoreAdapter;
45
43
  logger?: Logger;
46
44
  },
@@ -104,7 +102,6 @@ export class SequencerPublisherFactory {
104
102
  epochCache: this.deps.epochCache,
105
103
  governanceProposerContract: this.deps.governanceProposerContract,
106
104
  slashingProposerContract,
107
- slashFactoryContract: this.deps.slashFactoryContract,
108
105
  dateProvider: this.deps.dateProvider,
109
106
  metrics: this.publisherMetrics,
110
107
  lastActions: this.lastActions,