@aztec/ethereum 0.0.1-commit.04852196a → 0.0.1-commit.04d373f

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 (116) hide show
  1. package/dest/client.d.ts +16 -2
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +27 -7
  4. package/dest/config.d.ts +6 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +5 -9
  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/empire_base.d.ts +1 -3
  11. package/dest/contracts/empire_base.d.ts.map +1 -1
  12. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  13. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  14. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  15. package/dest/contracts/governance.d.ts +108 -33
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +193 -12
  18. package/dest/contracts/governance_proposer.d.ts +33 -3
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +51 -9
  21. package/dest/contracts/inbox.d.ts +3 -3
  22. package/dest/contracts/inbox.d.ts.map +1 -1
  23. package/dest/contracts/inbox.js +12 -7
  24. package/dest/contracts/index.d.ts +3 -3
  25. package/dest/contracts/index.d.ts.map +1 -1
  26. package/dest/contracts/index.js +2 -2
  27. package/dest/contracts/multicall.d.ts +127 -12
  28. package/dest/contracts/multicall.d.ts.map +1 -1
  29. package/dest/contracts/multicall.js +201 -94
  30. package/dest/contracts/outbox.d.ts +21 -4
  31. package/dest/contracts/outbox.d.ts.map +1 -1
  32. package/dest/contracts/outbox.js +29 -6
  33. package/dest/contracts/rollup.d.ts +5254 -25
  34. package/dest/contracts/rollup.d.ts.map +1 -1
  35. package/dest/contracts/rollup.js +350 -67
  36. package/dest/contracts/{tally_slashing_proposer.d.ts → slashing_proposer.d.ts} +3 -4
  37. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  38. package/dest/contracts/{tally_slashing_proposer.js → slashing_proposer.js} +13 -15
  39. package/dest/deploy_aztec_l1_contracts.d.ts +3 -6
  40. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  41. package/dest/deploy_aztec_l1_contracts.js +2 -4
  42. package/dest/generated/l1-contracts-defaults.d.ts +1 -1
  43. package/dest/generated/l1-contracts-defaults.js +1 -1
  44. package/dest/l1_artifacts.d.ts +13409 -15883
  45. package/dest/l1_artifacts.d.ts.map +1 -1
  46. package/dest/l1_artifacts.js +9 -24
  47. package/dest/l1_contract_addresses.d.ts +61 -65
  48. package/dest/l1_contract_addresses.d.ts.map +1 -1
  49. package/dest/l1_contract_addresses.js +101 -82
  50. package/dest/l1_reader.d.ts +5 -5
  51. package/dest/l1_reader.d.ts.map +1 -1
  52. package/dest/l1_reader.js +9 -7
  53. package/dest/l1_tx_utils/l1_tx_utils.d.ts +9 -1
  54. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  55. package/dest/l1_tx_utils/l1_tx_utils.js +54 -35
  56. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  57. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  58. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +8 -4
  59. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  60. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  61. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  62. package/dest/publisher_manager.d.ts +21 -7
  63. package/dest/publisher_manager.d.ts.map +1 -1
  64. package/dest/publisher_manager.js +81 -7
  65. package/dest/queries.d.ts +13 -2
  66. package/dest/queries.d.ts.map +1 -1
  67. package/dest/queries.js +62 -3
  68. package/dest/test/chain_monitor.d.ts +23 -3
  69. package/dest/test/chain_monitor.d.ts.map +1 -1
  70. package/dest/test/chain_monitor.js +38 -2
  71. package/dest/test/eth_cheat_codes.d.ts +12 -4
  72. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  73. package/dest/test/eth_cheat_codes.js +17 -4
  74. package/dest/test/rollup_cheat_codes.d.ts +31 -3
  75. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  76. package/dest/test/rollup_cheat_codes.js +59 -7
  77. package/dest/test/start_anvil.d.ts +15 -1
  78. package/dest/test/start_anvil.d.ts.map +1 -1
  79. package/dest/test/start_anvil.js +17 -2
  80. package/dest/utils.d.ts +8 -1
  81. package/dest/utils.d.ts.map +1 -1
  82. package/dest/utils.js +19 -0
  83. package/package.json +6 -6
  84. package/src/client.ts +28 -2
  85. package/src/config.ts +9 -13
  86. package/src/contracts/chain_state_override.ts +200 -0
  87. package/src/contracts/empire_base.ts +0 -2
  88. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  89. package/src/contracts/governance.ts +266 -9
  90. package/src/contracts/governance_proposer.ts +52 -5
  91. package/src/contracts/inbox.ts +13 -5
  92. package/src/contracts/index.ts +2 -2
  93. package/src/contracts/multicall.ts +243 -105
  94. package/src/contracts/outbox.ts +38 -8
  95. package/src/contracts/rollup.ts +391 -73
  96. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +14 -16
  97. package/src/deploy_aztec_l1_contracts.ts +1 -5
  98. package/src/generated/l1-contracts-defaults.ts +1 -1
  99. package/src/l1_artifacts.ts +12 -35
  100. package/src/l1_contract_addresses.ts +120 -94
  101. package/src/l1_reader.ts +17 -10
  102. package/src/l1_tx_utils/l1_tx_utils.ts +51 -28
  103. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +8 -4
  104. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  105. package/src/publisher_manager.ts +105 -10
  106. package/src/queries.ts +70 -4
  107. package/src/test/chain_monitor.ts +65 -3
  108. package/src/test/eth_cheat_codes.ts +16 -4
  109. package/src/test/rollup_cheat_codes.ts +64 -6
  110. package/src/test/start_anvil.ts +33 -2
  111. package/src/utils.ts +18 -0
  112. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  113. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  114. package/dest/contracts/empire_slashing_proposer.js +0 -216
  115. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  116. package/src/contracts/empire_slashing_proposer.ts +0 -265
@@ -130,9 +130,10 @@ export class ReadOnlyL1TxUtils {
130
130
  const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS);
131
131
  for (let i = 0; i < numBlocks; i++) {
132
132
  // each block can go up 12.5% from previous baseFee
133
- maxFeePerGas = (maxFeePerGas * (1_000n + 125n)) / 1_000n;
133
+ // ceil, (a+b-1)/b, to avoid truncation at small values (e.g. 1 wei blob base fee)
134
+ maxFeePerGas = (maxFeePerGas * (1_000n + 125n) + 999n) / 1_000n;
134
135
  // same for blob gas fee
135
- maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n;
136
+ maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n) + 999n) / 1_000n;
136
137
  }
137
138
 
138
139
  if (attempt > 0) {
@@ -242,13 +243,16 @@ export class ReadOnlyL1TxUtils {
242
243
  const gasConfig = { ...this.config, ..._gasConfig };
243
244
  let initialEstimate = 0n;
244
245
  if (_blobInputs) {
245
- // @note requests with blobs also require maxFeePerBlobGas to be set
246
+ // @note requests with blobs also require maxFeePerBlobGas to be set.
247
+ // Use 2x buffer for maxFeePerBlobGas to avoid stale fees and to pass EIP-4844 validation (even if it is a gas estimation call).
248
+ // 1. maxFeePerBlobGas >= blobBaseFee
249
+ // 2. account balance >= gas * maxFeePerGas + maxFeePerBlobGas * blobCount + value
246
250
  const gasPrice = await this.getGasPrice(gasConfig, true, 0);
247
251
  initialEstimate = await this.client.estimateGas({
248
252
  account,
249
253
  ...request,
250
254
  ..._blobInputs,
251
- maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!,
255
+ maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! * 2n,
252
256
  gas: MAX_L1_TX_LIMIT,
253
257
  blockTag: 'latest',
254
258
  });
@@ -81,7 +81,11 @@ export class Delayer {
81
81
 
82
82
  public maxInclusionTimeIntoSlot: number | undefined = undefined;
83
83
  public ethereumSlotDuration: bigint;
84
- public nextWait: { l1Timestamp: bigint } | { l1BlockNumber: bigint } | { indefinitely: true } | undefined = undefined;
84
+ public nextWait:
85
+ | { l1Timestamp: bigint; timeoutSeconds?: number }
86
+ | { l1BlockNumber: bigint; timeoutSeconds?: number }
87
+ | { indefinitely: true }
88
+ | undefined = undefined;
85
89
  public sentTxHashes: Hex[] = [];
86
90
  public cancelledTxs: Hex[] = [];
87
91
 
@@ -110,13 +114,13 @@ export class Delayer {
110
114
  }
111
115
 
112
116
  /** Delays the next tx to be sent so it lands on the given L1 block number. */
113
- pauseNextTxUntilBlock(l1BlockNumber: number | bigint) {
114
- this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber) };
117
+ pauseNextTxUntilBlock(l1BlockNumber: number | bigint, timeoutSeconds?: number) {
118
+ this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber), timeoutSeconds };
115
119
  }
116
120
 
117
121
  /** Delays the next tx to be sent so it lands on the given timestamp. */
118
- pauseNextTxUntilTimestamp(l1Timestamp: number | bigint) {
119
- this.nextWait = { l1Timestamp: BigInt(l1Timestamp) };
122
+ pauseNextTxUntilTimestamp(l1Timestamp: number | bigint, timeoutSeconds?: number) {
123
+ this.nextWait = { l1Timestamp: BigInt(l1Timestamp), timeoutSeconds };
120
124
  }
121
125
 
122
126
  /** Delays the next tx to be sent indefinitely. */
@@ -198,9 +202,14 @@ export function wrapClientWithDelayer<T extends ViemClient>(client: T, delayer:
198
202
  // Or wait until the desired block number or timestamp
199
203
  wait =
200
204
  'l1BlockNumber' in waitUntil
201
- ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger)
205
+ ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger, waitUntil.timeoutSeconds)
202
206
  : 'l1Timestamp' in waitUntil
203
- ? waitUntilL1Timestamp(publicClient, waitUntil.l1Timestamp - delayer.ethereumSlotDuration, logger)
207
+ ? waitUntilL1Timestamp(
208
+ publicClient,
209
+ waitUntil.l1Timestamp - delayer.ethereumSlotDuration,
210
+ logger,
211
+ waitUntil.timeoutSeconds,
212
+ )
204
213
  : undefined;
205
214
 
206
215
  logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`, {
@@ -1,6 +1,8 @@
1
1
  import { pick } from '@aztec/foundation/collection';
2
2
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
3
+ import { RunningPromise } from '@aztec/foundation/running-promise';
3
4
 
5
+ import { Multicall3 } from './contracts/multicall.js';
4
6
  import { L1TxUtils, TxUtilsState } from './l1_tx_utils/index.js';
5
7
 
6
8
  // Defines the order in which we prioritise publishers based on their state (first is better)
@@ -27,24 +29,72 @@ const busyStates: TxUtilsState[] = [
27
29
 
28
30
  export type PublisherFilter<UtilsType extends L1TxUtils> = (utils: UtilsType) => boolean;
29
31
 
32
+ /** Config accepted by PublisherManager. */
33
+ type PublisherManagerConfig = {
34
+ publisherAllowInvalidStates?: boolean;
35
+ publisherFundingThreshold?: bigint;
36
+ publisherFundingAmount?: bigint;
37
+ };
38
+
30
39
  export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
31
40
  private log: Logger;
32
- private config: { publisherAllowInvalidStates?: boolean };
41
+ private config: PublisherManagerConfig;
42
+ private static readonly FUNDING_CHECK_INTERVAL_MS = 2 * 60 * 1000;
43
+ private funder?: UtilsType;
44
+ private fundingPromise?: RunningPromise;
33
45
 
34
46
  constructor(
35
47
  private publishers: UtilsType[],
36
- config: { publisherAllowInvalidStates?: boolean },
37
- bindings?: LoggerBindings,
48
+ config: PublisherManagerConfig,
49
+ opts?: { bindings?: LoggerBindings; funder?: UtilsType },
38
50
  ) {
39
- this.log = createLogger('publisher:manager', bindings);
51
+ this.funder = opts?.funder;
52
+ this.log = createLogger('publisher:manager', opts?.bindings);
40
53
  this.log.info(`PublisherManager initialized with ${publishers.length} publishers.`);
41
54
  this.publishers = publishers;
42
- this.config = pick(config, 'publisherAllowInvalidStates');
55
+ this.config = pick(config, 'publisherAllowInvalidStates', 'publisherFundingThreshold', 'publisherFundingAmount');
56
+
57
+ const hasThreshold = this.config.publisherFundingThreshold !== undefined;
58
+ const hasAmount = this.config.publisherFundingAmount !== undefined;
59
+ if (hasThreshold !== hasAmount) {
60
+ this.log.warn(`Incomplete funding config: both publisherFundingThreshold and publisherFundingAmount must be set`);
61
+ }
62
+
63
+ if (this.funder) {
64
+ const funderAddress = this.funder.getSenderAddress();
65
+ if (publishers.some(p => p.getSenderAddress().equals(funderAddress))) {
66
+ this.log.error(`Funding account ${funderAddress} is also a publisher, disabling funding to avoid self-funding`);
67
+ this.funder = undefined;
68
+ }
69
+ }
43
70
  }
44
71
 
45
- /** Loads the state of all publishers and resumes monitoring any pending txs */
46
- public async loadState(): Promise<void> {
47
- await Promise.all(this.publishers.map(pub => pub.loadStateAndResumeMonitoring()));
72
+ /** Loads the state of all publishers and the funder, and starts periodic funding checks. */
73
+ public async start(): Promise<void> {
74
+ await Promise.all([
75
+ ...this.publishers.map(pub => pub.loadStateAndResumeMonitoring()),
76
+ this.funder?.loadStateAndResumeMonitoring(),
77
+ ]);
78
+
79
+ if (
80
+ this.funder &&
81
+ this.config.publisherFundingThreshold !== undefined &&
82
+ this.config.publisherFundingAmount !== undefined
83
+ ) {
84
+ this.fundingPromise = new RunningPromise(
85
+ () => this.triggerFundingIfNeeded(),
86
+ this.log,
87
+ PublisherManager.FUNDING_CHECK_INTERVAL_MS,
88
+ );
89
+ this.fundingPromise.start();
90
+ }
91
+ }
92
+
93
+ /** Stops the funding loop and interrupts all publishers. */
94
+ public async stop(): Promise<void> {
95
+ await this.fundingPromise?.stop();
96
+ this.publishers.forEach(pub => pub.interrupt());
97
+ this.funder?.interrupt();
48
98
  }
49
99
 
50
100
  // Finds and prioritises available publishers based on
@@ -102,7 +152,52 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
102
152
  return sortedPublishers[0].publisher;
103
153
  }
104
154
 
105
- public interrupt() {
106
- this.publishers.forEach(pub => pub.interrupt());
155
+ /** Check all publisher balances and fund those below threshold. */
156
+ private async triggerFundingIfNeeded(): Promise<void> {
157
+ const { funder, config } = this;
158
+ if (!funder || config.publisherFundingThreshold === undefined || config.publisherFundingAmount === undefined) {
159
+ return;
160
+ }
161
+
162
+ const allBalances = await Promise.all(
163
+ this.publishers.map(async pub => ({ balance: await pub.getSenderBalance(), publisher: pub })),
164
+ );
165
+ const lowBalance = allBalances.filter(p => p.balance < config.publisherFundingThreshold!);
166
+ if (lowBalance.length === 0) {
167
+ return;
168
+ }
169
+
170
+ const fundingAmount = config.publisherFundingAmount!;
171
+ const funderBalance = await funder.getSenderBalance();
172
+
173
+ if (funderBalance < 10n * fundingAmount) {
174
+ this.log.warn(`Funding account balance is low`, { funderBalance, threshold: 10n * fundingAmount });
175
+ }
176
+ const affordableCount = Number(funderBalance / fundingAmount);
177
+ if (affordableCount === 0) {
178
+ this.log.error(`Funding account balance too low to fund any publisher`, { funderBalance, fundingAmount });
179
+ return;
180
+ }
181
+ if (affordableCount < lowBalance.length) {
182
+ this.log.warn(`Funder can only afford ${affordableCount}/${lowBalance.length} publishers`, {
183
+ funderBalance,
184
+ fundingAmount,
185
+ });
186
+ }
187
+
188
+ const toFund = lowBalance.slice(0, affordableCount).map(p => p.publisher);
189
+ await this.fundPublishers(toFund);
190
+ }
191
+
192
+ /** Fund publishers via a single Multicall3 aggregate3Value transaction. */
193
+ private async fundPublishers(publishers: UtilsType[]): Promise<void> {
194
+ const fundingAmount = this.config.publisherFundingAmount!;
195
+ const calls = publishers.map(pub => ({
196
+ to: pub.getSenderAddress().toString(),
197
+ value: fundingAmount,
198
+ }));
199
+
200
+ await Multicall3.forwardValue(calls, this.funder!, this.log);
201
+ this.log.info(`Funded ${publishers.length} publishers`);
107
202
  }
108
203
  }
package/src/queries.ts CHANGED
@@ -1,11 +1,77 @@
1
1
  import { EthAddress } from '@aztec/foundation/eth-address';
2
2
 
3
+ import { BaseError, type Block } from 'viem';
4
+
3
5
  import { DefaultL1ContractsConfig, type L1ContractsConfig } from './config.js';
4
6
  import { ReadOnlyGovernanceContract } from './contracts/governance.js';
5
7
  import { GovernanceProposerContract } from './contracts/governance_proposer.js';
6
8
  import { InboxContract } from './contracts/inbox.js';
7
9
  import { RollupContract } from './contracts/rollup.js';
8
- import type { ViemPublicClient } from './types.js';
10
+ import type { ViemClient, ViemPublicClient } from './types.js';
11
+
12
+ /**
13
+ * Returns the L1 finalized block, or `undefined` if the chain does not yet have one
14
+ * (common on freshly started devnets). Rethrows any other RPC error.
15
+ */
16
+ export async function getFinalizedL1Block(client: ViemClient): Promise<Block<bigint, false, 'finalized'> | undefined> {
17
+ try {
18
+ return await client.getBlock({ blockTag: 'finalized', includeTransactions: false });
19
+ } catch (err) {
20
+ if (isFinalizedBlockTagNotFoundError(err)) {
21
+ return undefined;
22
+ }
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ // Error messages returned by popular Ethereum execution clients when
28
+ // eth_getBlockByNumber / eth_call is called with blockTag "finalized" or
29
+ // "safe" and no such block has been produced yet:
30
+ //
31
+ // geth "finalized block not found"
32
+ // "safe block not found"
33
+ // reth "block not found: finalized"
34
+ // "block not found: safe"
35
+ // nethermind "Unknown block error" (same for both tags)
36
+ // besu "Unknown block"
37
+ // erigon 'block "finalized" not available (head block: N)'
38
+ // 'block "safe" not available (head block: N)'
39
+ //
40
+ // A combined regex covers all five:
41
+ const FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE =
42
+ /(finalized|safe) block not found|block not found: (finalized|safe)|unknown block|block "(finalized|safe)" not available/i;
43
+
44
+ /**
45
+ * Returns true if the error originates from an RPC call that failed because
46
+ * the "finalized" (or "safe") block tag is not yet available on the chain.
47
+ */
48
+ export function isFinalizedBlockTagNotFoundError(err: unknown): boolean {
49
+ if (!err) {
50
+ return false;
51
+ }
52
+
53
+ if (err instanceof BaseError) {
54
+ const hit = err.walk((e: any) => matchesFinalizedBlockTagNotFound(e));
55
+ if (hit) {
56
+ return true;
57
+ }
58
+ }
59
+
60
+ for (let cur: any = err, i = 0; cur && i < 10; cur = cur.cause, i++) {
61
+ if (matchesFinalizedBlockTagNotFound(cur)) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ function matchesFinalizedBlockTagNotFound(e: any): boolean {
69
+ if (!e) {
70
+ return false;
71
+ }
72
+ const text = `${e.details ?? ''} ${e.message ?? ''} ${e.shortMessage ?? ''}`;
73
+ return FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE.test(text);
74
+ }
9
75
 
10
76
  /** Reads the L1ContractsConfig from L1 contracts. */
11
77
  export async function getL1ContractsConfig(
@@ -76,8 +142,8 @@ export async function getL1ContractsConfig(
76
142
  slasherProposer?.getRoundSize() ?? 0n,
77
143
  slasherProposer?.getLifetimeInRounds() ?? 0n,
78
144
  slasherProposer?.getExecutionDelayInRounds() ?? 0n,
79
- slasherProposer?.type === 'tally' ? slasherProposer.getSlashOffsetInRounds() : 0n,
80
- slasherProposer?.type === 'tally' ? slasherProposer.getSlashingAmounts() : [0n, 0n, 0n],
145
+ slasherProposer?.getSlashOffsetInRounds() ?? 0n,
146
+ slasherProposer?.getSlashingAmounts() ?? [0n, 0n, 0n],
81
147
  slasher?.getVetoer() ?? EthAddress.ZERO,
82
148
  slasher?.getSlashingDisableDuration() ?? 0,
83
149
  rollup.getManaTarget(),
@@ -114,7 +180,7 @@ export async function getL1ContractsConfig(
114
180
  rollupVersion: Number(rollupVersion),
115
181
  genesisArchiveTreeRoot: genesisArchiveTreeRoot.toString(),
116
182
  exitDelaySeconds: Number(exitDelay),
117
- slasherFlavor: slasherProposer?.type ?? 'tally',
183
+ slasherEnabled: !!slasherProposer,
118
184
  slashingOffsetInRounds: Number(slashingOffsetInRounds),
119
185
  slashAmountSmall: slashingAmounts[0],
120
186
  slashAmountMedium: slashingAmounts[1],
@@ -1,4 +1,4 @@
1
- import type { RollupContract } from '@aztec/ethereum/contracts';
1
+ import type { ManaMinFeeComponents, RollupContract } from '@aztec/ethereum/contracts';
2
2
  import { InboxContract } from '@aztec/ethereum/contracts';
3
3
  import { CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
4
  import { EthAddress } from '@aztec/foundation/eth-address';
@@ -10,6 +10,20 @@ import { EventEmitter } from 'events';
10
10
 
11
11
  import type { ViemClient } from '../types.js';
12
12
 
13
+ /** L2 fee data reported by the chain monitor. */
14
+ export type L2FeeData = ManaMinFeeComponents & {
15
+ /** Total minimum fee per mana in Fee Juice (sum of sequencerCost + proverCost + congestionCost). */
16
+ minFeePerMana: bigint;
17
+ /** L1 base fee observed by the oracle. */
18
+ l1BaseFee: bigint;
19
+ /** L1 blob fee observed by the oracle. */
20
+ l1BlobFee: bigint;
21
+ /** ETH per fee asset exchange rate (1e12 precision). */
22
+ ethPerFeeAsset: bigint;
23
+ /** Mana target per checkpoint. */
24
+ manaTarget: bigint;
25
+ };
26
+
13
27
  export type ChainMonitorEventMap = {
14
28
  'l1-block': [{ l1BlockNumber: number; timestamp: bigint }];
15
29
  checkpoint: [
@@ -19,6 +33,7 @@ export type ChainMonitorEventMap = {
19
33
  'l2-messages': [{ totalL2Messages: number; l1BlockNumber: number }];
20
34
  'l2-epoch': [{ l2EpochNumber: EpochNumber; timestamp: bigint; committee: EthAddress[] | undefined }];
21
35
  'l2-slot': [{ l2SlotNumber: SlotNumber; timestamp: bigint }];
36
+ 'l2-fees': [L2FeeData];
22
37
  };
23
38
 
24
39
  /** Utility class that polls the chain on quick intervals and logs new L1 blocks, L2 blocks, and L2 proofs. */
@@ -45,6 +60,8 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
45
60
  public l2EpochNumber!: EpochNumber;
46
61
  /** Current L2 slot number */
47
62
  public l2SlotNumber!: SlotNumber;
63
+ /** Current L2 fee data (components of the minimum fee per mana). */
64
+ public l2FeeData!: L2FeeData;
48
65
 
49
66
  constructor(
50
67
  private readonly rollup: RollupContract,
@@ -77,7 +94,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
77
94
  }
78
95
  }
79
96
 
80
- private async getInbox() {
97
+ protected async getInbox() {
81
98
  if (!this.inbox) {
82
99
  const { inboxAddress } = await this.rollup.getRollupAddresses();
83
100
  this.inbox = new InboxContract(this.l1Client, inboxAddress);
@@ -158,7 +175,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
158
175
  this.l2EpochNumber = l2Epoch;
159
176
  committee = await this.rollup.getCurrentEpochCommittee();
160
177
  this.emit('l2-epoch', { l2EpochNumber: l2Epoch, timestamp, committee });
161
- msg += ` starting new epoch ${this.l2EpochNumber} `;
178
+ msg += ` starting new epoch ${this.l2EpochNumber}`;
162
179
  }
163
180
 
164
181
  if (l2SlotNumber !== this.l2SlotNumber) {
@@ -166,6 +183,13 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
166
183
  this.emit('l2-slot', { l2SlotNumber, timestamp });
167
184
  }
168
185
 
186
+ const feeData = await this.fetchFeeData(timestamp);
187
+ if (this.hasFeeDataChanged(feeData)) {
188
+ msg += ` with L2 min fee ${feeData.minFeePerMana}`;
189
+ this.l2FeeData = feeData;
190
+ this.emit('l2-fees', feeData);
191
+ }
192
+
169
193
  this.logger.info(msg, {
170
194
  currentTimestamp: this.dateProvider.nowInSeconds(),
171
195
  l1Timestamp: timestamp,
@@ -176,6 +200,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
176
200
  provenCheckpointNumber: this.provenCheckpointNumber,
177
201
  totalL2Messages: this.totalL2Messages,
178
202
  committee,
203
+ ...this.l2FeeData,
179
204
  });
180
205
 
181
206
  return this;
@@ -196,6 +221,11 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
196
221
  });
197
222
  }
198
223
 
224
+ public async waitUntilNextL2Slot(): Promise<void> {
225
+ const targetSlot = SlotNumber.add((await this.run()).l2SlotNumber, 1);
226
+ return this.waitUntilL2Slot(targetSlot);
227
+ }
228
+
199
229
  public waitUntilL1Block(block: number | bigint): Promise<void> {
200
230
  const targetBlock = typeof block === 'bigint' ? block.valueOf() : block;
201
231
  if (this.l1BlockNumber >= targetBlock) {
@@ -242,4 +272,36 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
242
272
  this.on('checkpoint', listener);
243
273
  });
244
274
  }
275
+
276
+ private async fetchFeeData(timestamp: bigint): Promise<L2FeeData> {
277
+ const [components, minFeePerMana, l1Fees, ethPerFeeAsset, manaTarget] = await Promise.all([
278
+ this.rollup.getManaMinFeeComponentsAt(timestamp, true),
279
+ this.rollup.getManaMinFeeAt(timestamp, true),
280
+ this.rollup.getL1FeesAt(timestamp),
281
+ this.rollup.getEthPerFeeAsset(),
282
+ this.rollup.getManaTarget(),
283
+ ]);
284
+ return {
285
+ ...components,
286
+ minFeePerMana,
287
+ l1BaseFee: l1Fees.baseFee,
288
+ l1BlobFee: l1Fees.blobFee,
289
+ ethPerFeeAsset,
290
+ manaTarget,
291
+ };
292
+ }
293
+
294
+ private hasFeeDataChanged(newData: L2FeeData): boolean {
295
+ if (!this.l2FeeData) {
296
+ return true;
297
+ }
298
+ return (
299
+ this.l2FeeData.sequencerCost !== newData.sequencerCost ||
300
+ this.l2FeeData.proverCost !== newData.proverCost ||
301
+ this.l2FeeData.congestionCost !== newData.congestionCost ||
302
+ this.l2FeeData.l1BaseFee !== newData.l1BaseFee ||
303
+ this.l2FeeData.l1BlobFee !== newData.l1BlobFee ||
304
+ this.l2FeeData.ethPerFeeAsset !== newData.ethPerFeeAsset
305
+ );
306
+ }
245
307
  }
@@ -85,14 +85,26 @@ export class EthCheatCodes {
85
85
  }
86
86
 
87
87
  /**
88
- * Get the current timestamp
89
- * @returns The current timestamp
88
+ * Get the timestamp of the latest mined L1 block.
89
+ * Note: this is NOT the current time — it's the discrete timestamp of the last block.
90
+ * Between blocks, the actual chain time advances but no new block reflects it.
91
+ * @returns The latest block timestamp in seconds
90
92
  */
91
- public async timestamp(): Promise<number> {
93
+ public async lastBlockTimestamp(): Promise<number> {
92
94
  const res = await this.doRpcCall('eth_getBlockByNumber', ['latest', true]);
93
95
  return parseInt(res.timestamp, 16);
94
96
  }
95
97
 
98
+ /**
99
+ * Get the timestamp of the pending L1 block — the block that anvil will mine next.
100
+ * Reflects any prior `setNextBlockTimestamp` call or the configured block interval.
101
+ * @returns The pending block timestamp in seconds.
102
+ */
103
+ public async nextBlockTimestamp(): Promise<number> {
104
+ const res = await this.doRpcCall('eth_getBlockByNumber', ['pending', false]);
105
+ return parseInt(res.timestamp, 16);
106
+ }
107
+
96
108
  /**
97
109
  * Advance the chain by a number of blocks
98
110
  * @param numberOfBlocks - The number of blocks to mine
@@ -552,7 +564,7 @@ export class EthCheatCodes {
552
564
  }
553
565
 
554
566
  public async syncDateProvider() {
555
- const timestamp = await this.timestamp();
567
+ const timestamp = await this.lastBlockTimestamp();
556
568
  if ('setTime' in this.dateProvider) {
557
569
  this.dateProvider.setTime(timestamp * 1000);
558
570
  }
@@ -15,6 +15,7 @@ import {
15
15
  getContract,
16
16
  hexToBigInt,
17
17
  http,
18
+ keccak256,
18
19
  } from 'viem';
19
20
 
20
21
  import { EthCheatCodes } from './eth_cheat_codes.js';
@@ -53,7 +54,17 @@ export class RollupCheatCodes {
53
54
  /** Returns the current slot */
54
55
  public async getSlot(): Promise<SlotNumber> {
55
56
  const ts = BigInt((await this.client.getBlock()).timestamp);
56
- return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([ts]));
57
+ return this.getSlotAt(ts);
58
+ }
59
+
60
+ /** Returns the slot number at a given timestamp. */
61
+ public async getSlotAt(timestamp: bigint): Promise<SlotNumber> {
62
+ return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([timestamp]));
63
+ }
64
+
65
+ /** Returns the timestamp for the start of a given slot. */
66
+ public async getTimestampForSlot(slot: SlotNumber): Promise<bigint> {
67
+ return await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
57
68
  }
58
69
 
59
70
  /** Returns the number of seconds until the start of the given slot based on L1 block timestamp. */
@@ -168,6 +179,21 @@ export class RollupCheatCodes {
168
179
  return [timestamp, nextSlot];
169
180
  }
170
181
 
182
+ /**
183
+ * Warps L1 time to the start of an explicit absolute slot. Unlike `advanceSlots(N)` which is
184
+ * relative to the current L1 timestamp at call time and therefore races with real-time
185
+ * progression between query and warp, this lands at the slot regardless of latency.
186
+ *
187
+ * Throws if the requested slot is in the past relative to current L1 time (anvil cannot
188
+ * rewind).
189
+ */
190
+ public async advanceToSlot(slot: SlotNumber) {
191
+ const timestamp = await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
192
+ await this.ethCheatCodes.warp(Number(timestamp), { silent: true, resetBlockInterval: true });
193
+ this.logger.warn(`Advanced to slot ${slot}`, { timestamp });
194
+ return timestamp;
195
+ }
196
+
171
197
  /**
172
198
  * Warps time in L1 equivalent to however many slots.
173
199
  * @param howMany - The number of slots to advance.
@@ -259,12 +285,14 @@ export class RollupCheatCodes {
259
285
  });
260
286
  }
261
287
 
262
- public insertOutbox(epoch: EpochNumber, outHash: bigint) {
288
+ public insertOutbox(epoch: EpochNumber, numCheckpointsInEpoch: number, outHash: bigint) {
263
289
  return this.ethCheatCodes.execWithPausedAnvil(async () => {
264
290
  const outboxAddress = await this.rollup.read.getOutbox();
265
- const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch);
291
+ const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch, numCheckpointsInEpoch);
266
292
  await this.ethCheatCodes.store(EthAddress.fromString(outboxAddress), epochRootSlot, outHash);
267
- this.logger.warn(`Advanced outbox to epoch ${epoch} with out hash ${outHash}`);
293
+ this.logger.warn(
294
+ `Advanced outbox to epoch ${epoch} numCheckpointsInEpoch ${numCheckpointsInEpoch} with out hash ${outHash}`,
295
+ );
268
296
  });
269
297
  }
270
298
 
@@ -313,7 +341,8 @@ export class RollupCheatCodes {
313
341
  }
314
342
 
315
343
  /**
316
- * Directly updates proving cost per mana.
344
+ * Directly updates proving cost per mana. Throws if the on-chain tx reverts
345
+ * (e.g. rate-limit cooldown, step cap, or floor) instead of silently succeeding.
317
346
  * @param ethValue - The new proving cost per mana in ETH
318
347
  */
319
348
  public async setProvingCostPerMana(ethValue: bigint) {
@@ -323,8 +352,37 @@ export class RollupCheatCodes {
323
352
  chain: this.client.chain,
324
353
  gasLimit: 1000000n,
325
354
  });
326
- await this.client.waitForTransactionReceipt({ hash });
355
+ const receipt = await this.client.waitForTransactionReceipt({ hash });
356
+ if (receipt.status !== 'success') {
357
+ throw new Error(
358
+ `setProvingCostPerMana(${ethValue}) reverted on L1 (tx ${hash}). ` +
359
+ `Likely FeeLib rate-limit (30-day cooldown or 1.5x step cap); ` +
360
+ `use clearProvingCostCooldown() between successive updates.`,
361
+ );
362
+ }
327
363
  this.logger.warn(`Updated proving cost per mana to ${ethValue}`);
328
364
  });
329
365
  }
366
+
367
+ /**
368
+ * Resets the 30-day proving-cost update cooldown enforced by FeeLib.updateProvingCostPerMana
369
+ * by zeroing `FeeStore.provingCostLastUpdate` directly in contract storage. Use between
370
+ * successive setProvingCostPerMana / bumpProvingCostPerMana calls so the later update can
371
+ * land instead of reverting. Does not touch L1 time, so PXE/tx-expiration state stays intact.
372
+ *
373
+ * @note This is tightly coupled to the `FeeStore` layout in
374
+ * l1-contracts/src/core/libraries/rollup/FeeLib.sol:
375
+ * slot + 0: CompressedFeeConfig config (uint256)
376
+ * slot + 1: L1GasOracleValues l1GasOracleValues (14+14+4 bytes, packed)
377
+ * slot + 2: uint64 provingCostLastUpdate (only member — zeroing the slot is safe)
378
+ * If the struct layout changes, update the offset below.
379
+ */
380
+ public async clearProvingCostCooldown() {
381
+ const feeStoreBaseSlot = hexToBigInt(keccak256(Buffer.from('aztec.fee.storage', 'utf-8')));
382
+ const provingCostLastUpdateSlot = feeStoreBaseSlot + 2n;
383
+ await this.ethCheatCodes.store(EthAddress.fromString(this.rollup.address), provingCostLastUpdateSlot, 0n, {
384
+ silent: true,
385
+ });
386
+ this.logger.warn(`Cleared proving-cost update cooldown`);
387
+ }
330
388
  }
@@ -1,5 +1,6 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
  import { makeBackoff, retry } from '@aztec/foundation/retry';
3
+ import type { TestDateProvider } from '@aztec/foundation/timer';
3
4
  import { fileURLToPath } from '@aztec/foundation/url';
4
5
 
5
6
  import { type ChildProcess, spawn } from 'child_process';
@@ -26,6 +27,19 @@ export async function startAnvil(
26
27
  chainId?: number;
27
28
  /** The hardfork to use (e.g. 'cancun', 'latest'). */
28
29
  hardfork?: string;
30
+ /**
31
+ * Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags.
32
+ * Anvil reports `finalized = latest - slotsInAnEpoch * 2`.
33
+ * Defaults to 1 so the finalized block advances immediately, making tests that check
34
+ * L1-finality-based logic work without needing hundreds of mined blocks.
35
+ */
36
+ slotsInAnEpoch?: number;
37
+ /**
38
+ * If provided, the date provider will be synced to anvil's block time on every mined block.
39
+ * This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between
40
+ * the wall clock and the L1 chain when computing L1 slot timestamps.
41
+ */
42
+ dateProvider?: TestDateProvider;
29
43
  } = {},
30
44
  ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise<void> }> {
31
45
  const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
@@ -55,6 +69,7 @@ export async function startAnvil(
55
69
  if (opts.hardfork !== undefined) {
56
70
  args.push('--hardfork', opts.hardfork);
57
71
  }
72
+ args.push('--slots-in-an-epoch', String(opts.slotsInAnEpoch ?? 1));
58
73
 
59
74
  const child = spawn(anvilBinary, args, {
60
75
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -100,12 +115,15 @@ export async function startAnvil(
100
115
  child.once('close', onClose);
101
116
  });
102
117
 
103
- // Continue piping for logging / method-call capture after startup.
104
- if (logger || opts.captureMethodCalls) {
118
+ // Continue piping for logging, method-call capture, and/or dateProvider sync after startup.
119
+ if (logger || opts.captureMethodCalls || opts.dateProvider) {
105
120
  child.stdout?.on('data', (data: Buffer) => {
106
121
  const text = data.toString();
107
122
  logger?.debug(text.trim());
108
123
  methodCalls?.push(...(text.match(/eth_[^\s]+/g) || []));
124
+ if (opts.dateProvider) {
125
+ syncDateProviderFromAnvilOutput(text, opts.dateProvider);
126
+ }
109
127
  });
110
128
  child.stderr?.on('data', (data: Buffer) => {
111
129
  logger?.debug(data.toString().trim());
@@ -152,6 +170,19 @@ export async function startAnvil(
152
170
  return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` };
153
171
  }
154
172
 
173
+ /** Extracts block time from anvil stdout and syncs the dateProvider. */
174
+ function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDateProvider): void {
175
+ // Anvil logs mined blocks as:
176
+ // Block Time: "Fri, 20 Mar 2026 02:10:46 +0000"
177
+ const match = text.match(/Block Time:\s*"([^"]+)"/);
178
+ if (match) {
179
+ const blockTimeMs = new Date(match[1]).getTime();
180
+ if (!isNaN(blockTimeMs)) {
181
+ dateProvider.setTime(blockTimeMs);
182
+ }
183
+ }
184
+ }
185
+
155
186
  /** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */
156
187
  function killChild(child: ChildProcess): Promise<void> {
157
188
  return new Promise<void>(resolve => {