@aztec/ethereum 0.0.1-commit.4d79d1f2d → 0.0.1-commit.4d9804df

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 (176) hide show
  1. package/dest/client.d.ts +25 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +60 -8
  4. package/dest/config.d.ts +29 -8
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +68 -13
  7. package/dest/contracts/chain_state_override.d.ts +78 -0
  8. package/dest/contracts/chain_state_override.d.ts.map +1 -0
  9. package/dest/contracts/chain_state_override.js +137 -0
  10. package/dest/contracts/fee_asset_price_oracle.d.ts +105 -0
  11. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
  12. package/dest/contracts/fee_asset_price_oracle.js +656 -0
  13. package/dest/contracts/governance.d.ts +108 -33
  14. package/dest/contracts/governance.d.ts.map +1 -1
  15. package/dest/contracts/governance.js +196 -15
  16. package/dest/contracts/governance_proposer.d.ts +33 -1
  17. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  18. package/dest/contracts/governance_proposer.js +53 -2
  19. package/dest/contracts/gse.d.ts +4 -2
  20. package/dest/contracts/gse.d.ts.map +1 -1
  21. package/dest/contracts/gse.js +2 -2
  22. package/dest/contracts/inbox.d.ts +3 -3
  23. package/dest/contracts/inbox.d.ts.map +1 -1
  24. package/dest/contracts/inbox.js +12 -7
  25. package/dest/contracts/index.d.ts +4 -3
  26. package/dest/contracts/index.d.ts.map +1 -1
  27. package/dest/contracts/index.js +3 -2
  28. package/dest/contracts/multicall.d.ts +130 -13
  29. package/dest/contracts/multicall.d.ts.map +1 -1
  30. package/dest/contracts/multicall.js +203 -94
  31. package/dest/contracts/outbox.d.ts +27 -4
  32. package/dest/contracts/outbox.d.ts.map +1 -1
  33. package/dest/contracts/outbox.js +34 -7
  34. package/dest/contracts/registry.d.ts +3 -1
  35. package/dest/contracts/registry.d.ts.map +1 -1
  36. package/dest/contracts/registry.js +30 -1
  37. package/dest/contracts/rollup.d.ts +5419 -31
  38. package/dest/contracts/rollup.d.ts.map +1 -1
  39. package/dest/contracts/rollup.js +400 -75
  40. package/dest/contracts/{tally_slashing_proposer.d.ts → slashing_proposer.d.ts} +3 -4
  41. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  42. package/dest/contracts/{tally_slashing_proposer.js → slashing_proposer.js} +13 -15
  43. package/dest/deploy_aztec_l1_contracts.d.ts +19 -12
  44. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  45. package/dest/deploy_aztec_l1_contracts.js +61 -36
  46. package/dest/deploy_l1_contract.js +3 -3
  47. package/dest/foundry_binary.d.ts +13 -0
  48. package/dest/foundry_binary.d.ts.map +1 -0
  49. package/dest/foundry_binary.js +52 -0
  50. package/dest/l1_artifacts.d.ts +15524 -15847
  51. package/dest/l1_artifacts.d.ts.map +1 -1
  52. package/dest/l1_artifacts.js +9 -24
  53. package/dest/l1_contract_addresses.d.ts +61 -65
  54. package/dest/l1_contract_addresses.d.ts.map +1 -1
  55. package/dest/l1_contract_addresses.js +101 -82
  56. package/dest/l1_reader.d.ts +5 -5
  57. package/dest/l1_reader.d.ts.map +1 -1
  58. package/dest/l1_reader.js +9 -7
  59. package/dest/l1_tx_utils/config.d.ts +7 -1
  60. package/dest/l1_tx_utils/config.d.ts.map +1 -1
  61. package/dest/l1_tx_utils/config.js +17 -4
  62. package/dest/l1_tx_utils/factory.d.ts +18 -10
  63. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  64. package/dest/l1_tx_utils/factory.js +17 -7
  65. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts +15 -15
  66. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts.map +1 -1
  67. package/dest/l1_tx_utils/forwarder_l1_tx_utils.js +9 -15
  68. package/dest/l1_tx_utils/index-blobs.d.ts +3 -3
  69. package/dest/l1_tx_utils/index-blobs.d.ts.map +1 -1
  70. package/dest/l1_tx_utils/index-blobs.js +2 -2
  71. package/dest/l1_tx_utils/index.d.ts +2 -1
  72. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  73. package/dest/l1_tx_utils/index.js +1 -0
  74. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
  75. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
  76. package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
  77. package/dest/l1_tx_utils/l1_tx_utils.d.ts +26 -7
  78. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  79. package/dest/l1_tx_utils/l1_tx_utils.js +179 -82
  80. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  81. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  82. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +10 -5
  83. package/dest/l1_tx_utils/tx_delayer.d.ts +58 -0
  84. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -0
  85. package/dest/{test → l1_tx_utils}/tx_delayer.js +67 -37
  86. package/dest/l1_tx_utils/types.d.ts +27 -1
  87. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  88. package/dest/l1_tx_utils/types.js +10 -0
  89. package/dest/publisher_manager.d.ts +34 -8
  90. package/dest/publisher_manager.d.ts.map +1 -1
  91. package/dest/publisher_manager.js +119 -8
  92. package/dest/queries.d.ts +13 -2
  93. package/dest/queries.d.ts.map +1 -1
  94. package/dest/queries.js +69 -4
  95. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  96. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  97. package/dest/test/blob_kzg_warmup.js +64 -0
  98. package/dest/test/chain_monitor.d.ts +55 -4
  99. package/dest/test/chain_monitor.d.ts.map +1 -1
  100. package/dest/test/chain_monitor.js +104 -3
  101. package/dest/test/eth_cheat_codes.d.ts +37 -6
  102. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  103. package/dest/test/eth_cheat_codes.js +89 -41
  104. package/dest/test/index.d.ts +2 -3
  105. package/dest/test/index.d.ts.map +1 -1
  106. package/dest/test/index.js +1 -2
  107. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  108. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  109. package/dest/test/rollup_cheat_codes.js +86 -7
  110. package/dest/test/start_anvil.d.ts +23 -3
  111. package/dest/test/start_anvil.d.ts.map +1 -1
  112. package/dest/test/start_anvil.js +182 -32
  113. package/dest/test/upgrade_utils.js +2 -2
  114. package/dest/utils.d.ts +9 -2
  115. package/dest/utils.d.ts.map +1 -1
  116. package/dest/utils.js +48 -16
  117. package/package.json +6 -9
  118. package/src/client.ts +66 -2
  119. package/src/config.ts +96 -17
  120. package/src/contracts/chain_state_override.ts +200 -0
  121. package/src/contracts/fee_asset_price_oracle.ts +285 -0
  122. package/src/contracts/governance.ts +269 -12
  123. package/src/contracts/governance_proposer.ts +54 -1
  124. package/src/contracts/gse.ts +7 -2
  125. package/src/contracts/inbox.ts +13 -5
  126. package/src/contracts/index.ts +3 -2
  127. package/src/contracts/multicall.ts +247 -106
  128. package/src/contracts/outbox.ts +42 -8
  129. package/src/contracts/registry.ts +31 -1
  130. package/src/contracts/rollup.ts +457 -86
  131. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +14 -16
  132. package/src/deploy_aztec_l1_contracts.ts +88 -47
  133. package/src/deploy_l1_contract.ts +3 -3
  134. package/src/foundry_binary.ts +57 -0
  135. package/src/l1_artifacts.ts +12 -35
  136. package/src/l1_contract_addresses.ts +120 -94
  137. package/src/l1_reader.ts +17 -10
  138. package/src/l1_tx_utils/config.ts +23 -3
  139. package/src/l1_tx_utils/factory.ts +31 -31
  140. package/src/l1_tx_utils/forwarder_l1_tx_utils.ts +43 -54
  141. package/src/l1_tx_utils/index-blobs.ts +2 -2
  142. package/src/l1_tx_utils/index.ts +1 -0
  143. package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
  144. package/src/l1_tx_utils/l1_tx_utils.ts +164 -62
  145. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +10 -5
  146. package/src/{test → l1_tx_utils}/tx_delayer.ts +93 -56
  147. package/src/l1_tx_utils/types.ts +32 -0
  148. package/src/publisher_manager.ts +143 -12
  149. package/src/queries.ts +76 -4
  150. package/src/test/blob_kzg_warmup.ts +65 -0
  151. package/src/test/chain_monitor.ts +148 -3
  152. package/src/test/eth_cheat_codes.ts +82 -45
  153. package/src/test/index.ts +1 -2
  154. package/src/test/rollup_cheat_codes.ts +115 -6
  155. package/src/test/start_anvil.ts +212 -32
  156. package/src/test/upgrade_utils.ts +2 -2
  157. package/src/utils.ts +45 -18
  158. package/dest/contracts/empire_slashing_proposer.d.ts +0 -67
  159. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  160. package/dest/contracts/empire_slashing_proposer.js +0 -207
  161. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  162. package/dest/generated/l1-contracts-defaults.d.ts +0 -30
  163. package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
  164. package/dest/generated/l1-contracts-defaults.js +0 -30
  165. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +0 -26
  166. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +0 -1
  167. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +0 -26
  168. package/dest/test/delayed_tx_utils.d.ts +0 -13
  169. package/dest/test/delayed_tx_utils.d.ts.map +0 -1
  170. package/dest/test/delayed_tx_utils.js +0 -28
  171. package/dest/test/tx_delayer.d.ts +0 -36
  172. package/dest/test/tx_delayer.d.ts.map +0 -1
  173. package/src/contracts/empire_slashing_proposer.ts +0 -259
  174. package/src/generated/l1-contracts-defaults.ts +0 -32
  175. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +0 -77
  176. package/src/test/delayed_tx_utils.ts +0 -52
package/src/config.ts CHANGED
@@ -2,15 +2,15 @@ import {
2
2
  type ConfigMappingsType,
3
3
  bigintConfigHelper,
4
4
  booleanConfigHelper,
5
- enumConfigHelper,
6
5
  getConfigFromMappings,
7
6
  getDefaultConfig,
8
7
  numberConfigHelper,
8
+ omitConfigMappings,
9
9
  optionalNumberConfigHelper,
10
10
  } from '@aztec/foundation/config';
11
11
  import { EthAddress } from '@aztec/foundation/eth-address';
12
+ import l1ContractsDefaultEnv from '@aztec/l1-artifacts/network-defaults.json' with { type: 'json' };
12
13
 
13
- import { l1ContractsDefaultEnv } from './generated/l1-contracts-defaults.js';
14
14
  import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './l1_tx_utils/config.js';
15
15
 
16
16
  export type GenesisStateConfig = {
@@ -18,6 +18,8 @@ export type GenesisStateConfig = {
18
18
  testAccounts: boolean;
19
19
  /** Whether to populate the genesis state with initial fee juice for the sponsored FPC */
20
20
  sponsoredFPC: boolean;
21
+ /** Additional addresses to prefund with fee juice at genesis */
22
+ prefundAddresses: string[];
21
23
  };
22
24
 
23
25
  export type L1ContractsConfig = {
@@ -57,13 +59,13 @@ export type L1ContractsConfig = {
57
59
  slashingOffsetInRounds: number;
58
60
  /** How long slashing can be disabled for in seconds when vetoer disables it */
59
61
  slashingDisableDuration: number;
60
- /** Type of slasher proposer */
61
- slasherFlavor: 'empire' | 'tally' | 'none';
62
- /** Minimum amount that can be slashed in tally slashing */
62
+ /** Whether to deploy a slasher proposer */
63
+ slasherEnabled: boolean;
64
+ /** Minimum amount that can be slashed */
63
65
  slashAmountSmall: bigint;
64
- /** Medium amount to slash in tally slashing */
66
+ /** Medium amount to slash */
65
67
  slashAmountMedium: bigint;
66
- /** Largest amount that can be slashed per round in tally slashing */
68
+ /** Largest amount that can be slashed per round */
67
69
  slashAmountLarge: bigint;
68
70
  /** Governance proposing quorum (defaults to roundSize/2 + 1) */
69
71
  governanceProposerQuorum?: number;
@@ -79,11 +81,21 @@ export type L1ContractsConfig = {
79
81
  initialEthPerFeeAsset: bigint;
80
82
  /** The number of seconds to wait for an exit */
81
83
  exitDelaySeconds: number;
84
+ /** Validator set size at or below which the entry queue uses the bootstrap flush size. */
85
+ entryQueueBootstrapValidatorSetSize: number;
86
+ /** Number of validators admitted from the entry queue per flush during the bootstrap phase. */
87
+ entryQueueBootstrapFlushSize: number;
88
+ /** Minimum number of validators admitted from the entry queue per flush. */
89
+ entryQueueFlushSizeMin: number;
90
+ /** Divisor applied to the queue size to derive the normal per-flush admission count. */
91
+ entryQueueFlushSizeQuotient: number;
92
+ /** Maximum number of validators admitted from the entry queue per flush. */
93
+ entryQueueMaxFlushSize: number;
82
94
  } & L1TxUtilsConfig;
83
95
 
84
96
  /**
85
97
  * Config mappings for L1ContractsConfig.
86
- * Default values come from generated l1-contracts-defaults.json (source: defaults.yml).
98
+ * Default values come from l1-contracts/scripts/network-defaults.json (published via @aztec/l1-artifacts).
87
99
  * Real deployments use forge scripts which require explicit env vars (vm.envUint).
88
100
  */
89
101
  export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> = {
@@ -149,13 +161,10 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
149
161
  'How many slashing rounds back we slash (ie when slashing in round N, we slash for offenses committed during epochs of round N-offset)',
150
162
  ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_SLASHING_OFFSET_IN_ROUNDS),
151
163
  },
152
- slasherFlavor: {
153
- env: 'AZTEC_SLASHER_FLAVOR',
154
- description: 'Type of slasher proposer (empire, tally, or none)',
155
- ...enumConfigHelper(
156
- ['empire', 'tally', 'none'] as const,
157
- l1ContractsDefaultEnv.AZTEC_SLASHER_FLAVOR as 'empire' | 'tally' | 'none',
158
- ),
164
+ slasherEnabled: {
165
+ env: 'AZTEC_SLASHER_ENABLED',
166
+ description: 'Whether to deploy a slasher proposer',
167
+ ...booleanConfigHelper(true),
159
168
  },
160
169
  slashAmountSmall: {
161
170
  env: 'AZTEC_SLASH_AMOUNT_SMALL',
@@ -238,15 +247,75 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
238
247
  description: 'The delay before a validator can exit the set',
239
248
  ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_EXIT_DELAY_SECONDS),
240
249
  },
241
- ...l1TxUtilsConfigMappings,
250
+ entryQueueBootstrapValidatorSetSize: {
251
+ env: 'AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE',
252
+ description: 'Validator set size at or below which the entry queue uses the bootstrap flush size.',
253
+ ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE),
254
+ },
255
+ entryQueueBootstrapFlushSize: {
256
+ env: 'AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE',
257
+ description: 'Number of validators admitted from the entry queue per flush during the bootstrap phase.',
258
+ ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE),
259
+ },
260
+ entryQueueFlushSizeMin: {
261
+ env: 'AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN',
262
+ description: 'Minimum number of validators admitted from the entry queue per flush.',
263
+ ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN),
264
+ },
265
+ entryQueueFlushSizeQuotient: {
266
+ env: 'AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT',
267
+ description: 'Divisor applied to the queue size to derive the normal per-flush admission count.',
268
+ ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT),
269
+ },
270
+ entryQueueMaxFlushSize: {
271
+ env: 'AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE',
272
+ description: 'Maximum number of validators admitted from the entry queue per flush.',
273
+ ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE),
274
+ },
275
+ ...omitConfigMappings(l1TxUtilsConfigMappings, ['ethereumSlotDuration']),
242
276
  };
243
277
 
244
278
  /**
245
279
  * Default L1 contracts configuration derived from l1ContractsConfigMappings.
246
- * Source of truth: spartan/environments/defaults.yml -> defaults.l1-contracts
280
+ * Source of truth: l1-contracts/scripts/network-defaults.json (published via @aztec/l1-artifacts).
247
281
  */
248
282
  export const DefaultL1ContractsConfig = getDefaultConfig(l1ContractsConfigMappings);
249
283
 
284
+ /**
285
+ * Validates that `ethereumSlotDuration` and `aztecSlotDuration` are positive and that the L2 slot is an exact
286
+ * multiple of the L1 slot. Every L2 slot boundary must land on an L1 slot boundary; a non-multiple pairing
287
+ * desyncs the epoch/checkpoint timing math throughout the sequencer, validator client, and timetables. Returns
288
+ * a list of error messages (empty when valid).
289
+ */
290
+ export function validateSlotDurations(
291
+ config: Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'>,
292
+ ): string[] {
293
+ const errors: string[] = [];
294
+ if (config.ethereumSlotDuration <= 0) {
295
+ errors.push(`ethereumSlotDuration must be positive (got ${config.ethereumSlotDuration})`);
296
+ }
297
+ if (config.aztecSlotDuration <= 0) {
298
+ errors.push(`aztecSlotDuration must be positive (got ${config.aztecSlotDuration})`);
299
+ }
300
+ if (config.ethereumSlotDuration > 0 && config.aztecSlotDuration % config.ethereumSlotDuration !== 0) {
301
+ errors.push(
302
+ `aztecSlotDuration (${config.aztecSlotDuration}s) must be a multiple of ethereumSlotDuration ` +
303
+ `(${config.ethereumSlotDuration}s)`,
304
+ );
305
+ }
306
+ return errors;
307
+ }
308
+
309
+ /** Throws if {@link validateSlotDurations} reports any errors. */
310
+ export function assertValidSlotDurations(
311
+ config: Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'>,
312
+ ): void {
313
+ const errors = validateSlotDurations(config);
314
+ if (errors.length > 0) {
315
+ throw new Error(`Invalid slot duration configuration:\n${errors.map(e => ` - ${e}`).join('\n')}`);
316
+ }
317
+ }
318
+
250
319
  export const genesisStateConfigMappings: ConfigMappingsType<GenesisStateConfig> = {
251
320
  testAccounts: {
252
321
  env: 'TEST_ACCOUNTS',
@@ -258,6 +327,16 @@ export const genesisStateConfigMappings: ConfigMappingsType<GenesisStateConfig>
258
327
  description: 'Whether to populate the genesis state with initial fee juice for the sponsored FPC.',
259
328
  ...booleanConfigHelper(false),
260
329
  },
330
+ prefundAddresses: {
331
+ env: 'PREFUND_ADDRESSES',
332
+ description: 'Comma-separated list of Aztec addresses to prefund with fee juice at genesis (local network only).',
333
+ parseEnv: (val: string) =>
334
+ val
335
+ .split(',')
336
+ .map(a => a.trim())
337
+ .filter(a => a.length > 0),
338
+ defaultValue: [],
339
+ },
261
340
  };
262
341
 
263
342
  export function getL1ContractsConfigEnvVars(): L1ContractsConfig {
@@ -0,0 +1,200 @@
1
+ import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
2
+ import type { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
+ import type { Buffer32 } from '@aztec/foundation/buffer';
4
+ import { merge } from '@aztec/foundation/collection';
5
+ import type { Fr } from '@aztec/foundation/curves/bn254';
6
+
7
+ import type { StateOverride } from 'viem';
8
+
9
+ import { type FeeHeader, RollupContract } from './rollup.js';
10
+
11
+ /**
12
+ * Override values for the pending checkpoint that the simulation should treat as already applied.
13
+ * Every field is optional at plan-building time so callers can populate them incrementally; whatever
14
+ * is present at translation time is forwarded to the partial `tempCheckpointLogs` helper so the
15
+ * load-bearing `slotNumber` can land even if other fields could not be derived locally.
16
+ */
17
+ export type PendingCheckpointOverrideState = {
18
+ archive?: Fr;
19
+ feeHeader?: FeeHeader;
20
+ headerHash?: Fr;
21
+ outHash?: Fr;
22
+ payloadDigest?: Buffer32;
23
+ slotNumber?: SlotNumber;
24
+ };
25
+
26
+ export type ChainTipsOverride = {
27
+ pending?: CheckpointNumber;
28
+ proven?: CheckpointNumber;
29
+ };
30
+
31
+ /** Describes the simulated L1 rollup state that downstream calls should observe. */
32
+ export type SimulationOverridesPlan = {
33
+ chainTipsOverride?: ChainTipsOverride;
34
+ pendingCheckpointState?: PendingCheckpointOverrideState;
35
+ disableBlobCheck?: boolean;
36
+ };
37
+
38
+ /** Builds a single-checkpoint simulation plan before it is translated into a viem state override. */
39
+ export class SimulationOverridesBuilder {
40
+ private chainTipsOverride?: ChainTipsOverride;
41
+ private pendingCheckpointState?: PendingCheckpointOverrideState;
42
+ private disableBlobCheck = false;
43
+
44
+ /** Starts from an existing plan so callers can extend or specialize it. */
45
+ public static from(plan: SimulationOverridesPlan | undefined): SimulationOverridesBuilder {
46
+ return new SimulationOverridesBuilder().merge(plan);
47
+ }
48
+
49
+ /**
50
+ * Merges another plan into this builder. Later values win on a per-half basis for chain tips,
51
+ * but explicit `undefined` fields in the incoming plan are ignored so they cannot erase a
52
+ * previously-set value.
53
+ */
54
+ public merge(plan: SimulationOverridesPlan | undefined): this {
55
+ if (!plan) {
56
+ return this;
57
+ }
58
+
59
+ if (plan.chainTipsOverride) {
60
+ this.chainTipsOverride = merge(this.chainTipsOverride ?? {}, plan.chainTipsOverride);
61
+ }
62
+ if (plan.pendingCheckpointState) {
63
+ this.pendingCheckpointState = merge(this.pendingCheckpointState ?? {}, plan.pendingCheckpointState);
64
+ }
65
+ this.disableBlobCheck = this.disableBlobCheck || (plan.disableBlobCheck ?? false);
66
+
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Sets the pending and/or proven checkpoint number overrides. Subsequent calls merge into the existing
72
+ * override on a per-half basis, so callers can set pending in one call and proven in another without
73
+ * clobbering each other.
74
+ */
75
+ public withChainTips(override: ChainTipsOverride): this {
76
+ this.chainTipsOverride = { ...(this.chainTipsOverride ?? {}), ...override };
77
+ return this;
78
+ }
79
+
80
+ /** Overrides the archive root for the configured pending checkpoint. */
81
+ public withPendingArchive(archive: Fr): this {
82
+ this.assertPendingCheckpointNumber();
83
+ this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), archive };
84
+ return this;
85
+ }
86
+
87
+ /** Overrides the fee header for the configured pending checkpoint. */
88
+ public withPendingFeeHeader(feeHeader: FeeHeader): this {
89
+ this.assertPendingCheckpointNumber();
90
+ this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), feeHeader };
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Overrides one or more `tempCheckpointLogs` cell fields for the configured pending checkpoint.
96
+ * Fields are independent: any subset can be provided. The translator (`makeTempCheckpointLogOverride`)
97
+ * emits a stateDiff entry per field actually set, so unspecified fields stay at their on-chain
98
+ * values.
99
+ *
100
+ * `slotNumber` is load-bearing for `STFLib.canPruneAtTime`: when the simulation overrides `pending`
101
+ * to a checkpoint that has no on-chain `tempCheckpointLogs` entry yet, the missing slotNumber falls
102
+ * back to 0 and the contract treats the pending tip as belonging to epoch 0, triggering a phantom
103
+ * prune that silently undoes the `pending` override.
104
+ */
105
+ public withPendingTempCheckpointLogFields(fields: {
106
+ headerHash?: Fr;
107
+ outHash?: Fr;
108
+ payloadDigest?: Buffer32;
109
+ slotNumber?: SlotNumber;
110
+ }): this {
111
+ this.assertPendingCheckpointNumber();
112
+ this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), ...fields };
113
+ return this;
114
+ }
115
+
116
+ /** Disables blob checking for simulations that cannot provide DA inputs. */
117
+ public withoutBlobCheck(): this {
118
+ this.disableBlobCheck = true;
119
+ return this;
120
+ }
121
+
122
+ /** Builds the final plan, or `undefined` when no overrides were configured. */
123
+ public build(): SimulationOverridesPlan | undefined {
124
+ if (!this.pendingCheckpointState && !this.chainTipsOverride && !this.disableBlobCheck) {
125
+ return undefined;
126
+ }
127
+
128
+ return {
129
+ chainTipsOverride: this.chainTipsOverride,
130
+ pendingCheckpointState: this.pendingCheckpointState,
131
+ disableBlobCheck: this.disableBlobCheck || undefined,
132
+ };
133
+ }
134
+
135
+ private assertPendingCheckpointNumber(): void {
136
+ if (this.chainTipsOverride?.pending === undefined) {
137
+ throw new Error('withChainTips({ pending }) must be called before attaching archive or fee header overrides');
138
+ }
139
+ }
140
+ }
141
+
142
+ /** Translates a simulation plan into the viem state override shape expected by rollup calls. */
143
+ export async function buildSimulationOverridesStateOverride(
144
+ rollup: RollupContract,
145
+ plan: SimulationOverridesPlan | undefined,
146
+ ): Promise<StateOverride> {
147
+ if (!plan) {
148
+ return [];
149
+ }
150
+
151
+ const rollupStateDiff: NonNullable<StateOverride[number]['stateDiff']> = [];
152
+
153
+ if (plan.chainTipsOverride) {
154
+ rollupStateDiff.push(...extractRollupStateDiff(await rollup.makeChainTipsOverride(plan.chainTipsOverride)));
155
+ }
156
+
157
+ if (plan.pendingCheckpointState && plan.chainTipsOverride?.pending === undefined) {
158
+ throw new Error('pendingCheckpointState requires chainTipsOverride.pending to be set');
159
+ }
160
+
161
+ if (plan.pendingCheckpointState?.archive) {
162
+ rollupStateDiff.push(
163
+ ...extractRollupStateDiff(
164
+ rollup.makeArchiveOverride(plan.chainTipsOverride!.pending!, plan.pendingCheckpointState.archive),
165
+ ),
166
+ );
167
+ }
168
+
169
+ if (plan.pendingCheckpointState) {
170
+ rollupStateDiff.push(
171
+ ...extractRollupStateDiff(
172
+ await rollup.makeTempCheckpointLogOverride(plan.chainTipsOverride!.pending!, {
173
+ headerHash: plan.pendingCheckpointState.headerHash,
174
+ outHash: plan.pendingCheckpointState.outHash,
175
+ payloadDigest: plan.pendingCheckpointState.payloadDigest,
176
+ slotNumber: plan.pendingCheckpointState.slotNumber,
177
+ feeHeader: plan.pendingCheckpointState.feeHeader,
178
+ }),
179
+ ),
180
+ );
181
+ }
182
+
183
+ if (plan.disableBlobCheck) {
184
+ rollupStateDiff.push({
185
+ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
186
+ value: toPaddedHex(0n, true),
187
+ });
188
+ }
189
+
190
+ if (rollupStateDiff.length === 0) {
191
+ return [];
192
+ }
193
+
194
+ return [{ address: rollup.address, stateDiff: rollupStateDiff }];
195
+ }
196
+
197
+ function extractRollupStateDiff(override: StateOverride | StateOverride[number] | undefined) {
198
+ const entries = Array.isArray(override) ? override : override ? [override] : [];
199
+ return entries.flatMap(entry => entry.stateDiff ?? []);
200
+ }
@@ -0,0 +1,285 @@
1
+ import { memoize } from '@aztec/foundation/decorators';
2
+ import { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { type Logger, createLogger } from '@aztec/foundation/log';
4
+
5
+ import { type Hex, encodeAbiParameters, getContract, keccak256, parseAbiParameters } from 'viem';
6
+
7
+ import type { ViemClient } from '../types.js';
8
+ import { RollupContract } from './rollup.js';
9
+
10
+ /** Maximum price modifier per checkpoint in basis points. ±100 bps = ±1% */
11
+ export const MAX_FEE_ASSET_PRICE_MODIFIER_BPS = 100n;
12
+
13
+ /**
14
+ * Validates that a fee asset price modifier is within the allowed range.
15
+ * Validators should call this before attesting to a checkpoint proposal.
16
+ *
17
+ * @param modifier - The fee asset price modifier in basis points
18
+ * @returns true if the modifier is valid (between -100 and +100 bps)
19
+ */
20
+ export function validateFeeAssetPriceModifier(modifier: bigint): boolean {
21
+ return modifier >= -MAX_FEE_ASSET_PRICE_MODIFIER_BPS && modifier <= MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
22
+ }
23
+
24
+ /**
25
+ * Oracle for computing fee asset price modifiers based on Uniswap V4 pool prices.
26
+ * Only active on Ethereum mainnet - returns 0 on other chains.
27
+ */
28
+ export class FeeAssetPriceOracle {
29
+ constructor(
30
+ private client: ViemClient,
31
+ private readonly rollupContract: RollupContract,
32
+ private log: Logger = createLogger('fee-asset-price-oracle'),
33
+ ) {}
34
+
35
+ @memoize
36
+ async getUniswapOracle(): Promise<UniswapPriceOracle | undefined> {
37
+ const code = await this.client.getCode({ address: STATE_VIEW_ADDRESS.toString() });
38
+ if (code === undefined || code === '0x') {
39
+ this.log.warn('Uniswap V4 StateView contract not found, skipping fee asset price oracle');
40
+ return undefined;
41
+ }
42
+ this.log.info('Uniswap V4 StateView contract found, initializing fee asset price oracle');
43
+ const oracle = new UniswapPriceOracle(this.client, this.log);
44
+
45
+ try {
46
+ if (!(await oracle.isPoolInitialized())) {
47
+ this.log.warn('Uniswap V4 pool not initialized, skipping fee asset price oracle');
48
+ return undefined;
49
+ }
50
+ } catch (err) {
51
+ this.log.warn(`Failed to check if Uniswap V4 pool is initialized: ${err}`);
52
+ return undefined;
53
+ }
54
+
55
+ return oracle;
56
+ }
57
+
58
+ /**
59
+ * Computes the fee asset price modifier to be used in the next checkpoint proposal.
60
+ *
61
+ * The modifier adjusts the on-chain fee asset price toward the oracle price,
62
+ * clamped to ±1% (±100 basis points) per checkpoint.
63
+ *
64
+ * Returns 0 if not on mainnet or if the oracle query fails.
65
+ *
66
+ * @param currentPriceE12 - Optional override for the parent checkpoint's eth-per-fee-asset
67
+ * (E12 scale). When omitted, the latest published value is read from L1. Pipelined
68
+ * proposers should supply the predicted parent value so the modifier they emit aligns
69
+ * with the parent fee header L1 will see when the previous pipelined checkpoint lands.
70
+ * @returns The price modifier in basis points (positive to increase price, negative to decrease)
71
+ */
72
+ async computePriceModifier(currentPriceE12?: bigint): Promise<bigint> {
73
+ const uniswapOracle = await this.getUniswapOracle();
74
+ if (!uniswapOracle) {
75
+ return 0n;
76
+ }
77
+
78
+ try {
79
+ // Get current on-chain price (ETH per fee asset, E12), preferring the caller-supplied value.
80
+ const resolvedCurrentPriceE12 = currentPriceE12 ?? (await this.rollupContract.getEthPerFeeAsset());
81
+
82
+ // Get oracle price (median of last N blocks, ETH per fee asset, E12)
83
+ const oraclePriceE12 = await uniswapOracle.getMeanEthPerFeeAssetE12();
84
+
85
+ // Compute modifier in basis points
86
+ const modifier = this.computePriceModifierBps(resolvedCurrentPriceE12, oraclePriceE12);
87
+
88
+ this.log.debug('Computed price modifier', {
89
+ currentPriceE12: resolvedCurrentPriceE12.toString(),
90
+ oraclePriceE12: oraclePriceE12.toString(),
91
+ modifierBps: modifier.toString(),
92
+ currentPriceFromCaller: currentPriceE12 !== undefined,
93
+ });
94
+
95
+ return modifier;
96
+ } catch (err) {
97
+ this.log.warn(`Failed to compute price modifier, using 0: ${err}`);
98
+ return 0n;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Gets the current oracle price (ETH per fee asset, scaled by 1e12).
104
+ * Returns undefined if not on mainnet or if the oracle query fails.
105
+ */
106
+ async getOraclePrice(): Promise<bigint | undefined> {
107
+ const uniswapOracle = await this.getUniswapOracle();
108
+ if (!uniswapOracle) {
109
+ return undefined;
110
+ }
111
+
112
+ try {
113
+ return await uniswapOracle.getMeanEthPerFeeAssetE12();
114
+ } catch (err) {
115
+ this.log.warn(`Failed to get oracle price: ${err}`);
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Computes the basis points modifier needed to move from current price toward target price.
122
+ *
123
+ * @param currentPrice - Current ETH per fee asset (E12 scale)
124
+ * @param targetPrice - Target ETH per fee asset (E12 scale)
125
+ * @returns Basis points modifier clamped to ±100 (±1%)
126
+ */
127
+ computePriceModifierBps(currentPrice: bigint, targetPrice: bigint): bigint {
128
+ if (currentPrice === 0n) {
129
+ return MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
130
+ }
131
+
132
+ // Calculate percentage difference in basis points
133
+ // modifierBps = ((targetPrice - currentPrice) / currentPrice) * 10000
134
+ const diff = targetPrice - currentPrice;
135
+ const rawModifierBps = (diff * 10_000n) / currentPrice;
136
+
137
+ // Clamp to ±MAX_FEE_ASSET_PRICE_MODIFIER_BPS
138
+ if (rawModifierBps > MAX_FEE_ASSET_PRICE_MODIFIER_BPS) {
139
+ return MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
140
+ }
141
+ if (rawModifierBps < -MAX_FEE_ASSET_PRICE_MODIFIER_BPS) {
142
+ return -MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
143
+ }
144
+ return rawModifierBps;
145
+ }
146
+ }
147
+
148
+ /** Mainnet Uniswap V4 StateView contract address */
149
+ export const STATE_VIEW_ADDRESS = EthAddress.fromString('0x7ffe42c4a5deea5b0fec41c94c136cf115597227');
150
+
151
+ const PRECISION_Q192 = 10n ** 12n * 2n ** 192n;
152
+
153
+ /**
154
+ * Converts Uniswap's sqrtPriceX96 directly to ETH per FeeAsset (E12).
155
+ *
156
+ * For an ETH/FeeAsset pool where ETH is currency0 and FeeAsset is currency1:
157
+ * - Uniswap's sqrtPriceX96 = sqrt(FeeAsset/ETH) * 2^96
158
+ * - We need: ETH/FeeAsset with 1e12 precision
159
+ *
160
+ * Math:
161
+ * price = (sqrtPriceX96 / 2^96)^2 = sqrtPriceX96^2 / 2^192 (FeeAsset per ETH)
162
+ * ethPerFeeAsset = 1 / price = 2^192 / sqrtPriceX96^2
163
+ * ethPerFeeAssetE12 = ethPerFeeAsset * 1e12 = 1e12 * 2^192 / sqrtPriceX96^2
164
+ */
165
+ export function sqrtPriceX96ToEthPerFeeAssetE12(sqrtPriceX96: bigint): bigint {
166
+ if (sqrtPriceX96 === 0n) {
167
+ throw new Error('Cannot convert zero sqrtPriceX96');
168
+ }
169
+ return PRECISION_Q192 / (sqrtPriceX96 * sqrtPriceX96);
170
+ }
171
+ /**
172
+ * Uniswap V4 StateView ABI - only the functions we need
173
+ */
174
+ const StateViewAbi = [
175
+ {
176
+ type: 'function',
177
+ name: 'getSlot0',
178
+ inputs: [{ name: 'poolId', type: 'bytes32', internalType: 'PoolId' }],
179
+ outputs: [
180
+ { name: 'sqrtPriceX96', type: 'uint160', internalType: 'uint160' },
181
+ { name: 'tick', type: 'int24', internalType: 'int24' },
182
+ { name: 'protocolFee', type: 'uint24', internalType: 'uint24' },
183
+ { name: 'lpFee', type: 'uint24', internalType: 'uint24' },
184
+ ],
185
+ stateMutability: 'view',
186
+ },
187
+ ] as const;
188
+
189
+ /**
190
+ * Client for querying the ETH/FeeAsset price from Uniswap V4.
191
+ * Returns prices in ETH per FeeAsset format (E12) to match the rollup contract.
192
+ */
193
+ class UniswapPriceOracle {
194
+ private readonly stateView;
195
+ private readonly poolId: Hex;
196
+ private readonly log: Logger;
197
+
198
+ constructor(
199
+ private readonly client: ViemClient,
200
+ log?: Logger,
201
+ ) {
202
+ this.log = log ?? createLogger('uniswap-price-oracle');
203
+ this.stateView = getContract({
204
+ address: STATE_VIEW_ADDRESS.toString(),
205
+ abi: StateViewAbi,
206
+ client,
207
+ });
208
+ this.poolId = this.computePoolId();
209
+ this.log.debug(`Initialized UniswapPriceOracle with poolId: ${this.poolId}`);
210
+ }
211
+
212
+ /**
213
+ * Computes the PoolId from the pool configuration by hashing its components.
214
+ * PoolId = keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks))
215
+ * For mainnet, the value is expected to be: 0xce2899b16743cfd5a954d8122d5e07f410305b1aebee39fd73d9f3b9ebf10c2f
216
+ * Derived anyway to make it simpler to change if needed.
217
+ */
218
+ @memoize
219
+ computePoolId(): Hex {
220
+ /** ETH/FeeAsset pool configuration (hardcoded for mainnet) */
221
+ const encoded = encodeAbiParameters(parseAbiParameters('address, address, uint24, int24, address'), [
222
+ EthAddress.ZERO.toString(),
223
+ EthAddress.fromString('0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2').toString(),
224
+ 500, // 0.05%
225
+ 10,
226
+ EthAddress.fromString('0xd53006d1e3110fD319a79AEEc4c527a0d265E080').toString(),
227
+ ]);
228
+ return keccak256(encoded);
229
+ }
230
+
231
+ async isPoolInitialized(): Promise<boolean> {
232
+ const [sqrtPriceX96] = await this.stateView.read.getSlot0([this.poolId], undefined);
233
+ return sqrtPriceX96 !== 0n;
234
+ }
235
+
236
+ /**
237
+ * Gets the price as ETH per FeeAsset, scaled by 1e12.
238
+ * This is the format expected by the rollup contract.
239
+ *
240
+ * @param blockNumber - Optional block number to query at (defaults to latest)
241
+ */
242
+ async getEthPerFeeAssetE12(blockNumber?: bigint): Promise<bigint> {
243
+ const [sqrtPriceX96] = await this.stateView.read.getSlot0(
244
+ [this.poolId],
245
+ blockNumber !== undefined ? { blockNumber } : undefined,
246
+ );
247
+ return sqrtPriceX96ToEthPerFeeAssetE12(sqrtPriceX96);
248
+ }
249
+
250
+ /**
251
+ * Gets the median price over the last N blocks as ETH per FeeAsset (E12).
252
+ * Using median helps protect against single-block manipulation.
253
+ *
254
+ * @param numBlocks - Number of recent blocks to sample (default: 5)
255
+ * @returns Median price as ETH per FeeAsset, scaled by 1e12
256
+ */
257
+ async getMeanEthPerFeeAssetE12(numBlocks: number = 5): Promise<bigint> {
258
+ const currentBlock = await this.client.getBlockNumber();
259
+ const prices: bigint[] = [];
260
+
261
+ for (let i = 0; i < numBlocks; i++) {
262
+ const blockNumber = currentBlock - BigInt(i);
263
+ if (blockNumber < 0n) {
264
+ break;
265
+ }
266
+
267
+ try {
268
+ const price = await this.getEthPerFeeAssetE12(blockNumber);
269
+ prices.push(price);
270
+ } catch (err) {
271
+ this.log.warn(`Failed to get price at block ${blockNumber}: ${err}`);
272
+ // Continue with fewer samples
273
+ }
274
+ }
275
+
276
+ const filteredPrices = prices.filter(price => price !== 0n);
277
+
278
+ if (filteredPrices.length === 0) {
279
+ throw new Error('Failed to get any price samples from Uniswap oracle');
280
+ }
281
+
282
+ const mean = filteredPrices.reduce((a, b) => a + b, 0n) / BigInt(filteredPrices.length);
283
+ return mean;
284
+ }
285
+ }