@aztec/ethereum 0.0.1-commit.2ed92850 → 0.0.1-commit.2f68f620

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 (166) 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 +8 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +15 -11
  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_handler.d.ts +1 -1
  11. package/dest/contracts/fee_asset_handler.d.ts.map +1 -1
  12. package/dest/contracts/fee_asset_handler.js +2 -0
  13. package/dest/contracts/fee_asset_price_oracle.d.ts +105 -0
  14. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
  15. package/dest/contracts/fee_asset_price_oracle.js +656 -0
  16. package/dest/contracts/governance.d.ts +108 -33
  17. package/dest/contracts/governance.d.ts.map +1 -1
  18. package/dest/contracts/governance.js +198 -15
  19. package/dest/contracts/governance_proposer.d.ts +33 -1
  20. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  21. package/dest/contracts/governance_proposer.js +57 -3
  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 +127 -12
  29. package/dest/contracts/multicall.d.ts.map +1 -1
  30. package/dest/contracts/multicall.js +201 -93
  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 +5259 -29
  38. package/dest/contracts/rollup.d.ts.map +1 -1
  39. package/dest/contracts/rollup.js +392 -71
  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} +18 -13
  43. package/dest/deploy_aztec_l1_contracts.d.ts +8 -11
  44. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  45. package/dest/deploy_aztec_l1_contracts.js +46 -34
  46. package/dest/deploy_l1_contract.js +3 -3
  47. package/dest/generated/l1-contracts-defaults.d.ts +2 -2
  48. package/dest/generated/l1-contracts-defaults.js +2 -2
  49. package/dest/l1_artifacts.d.ts +14259 -15724
  50. package/dest/l1_artifacts.d.ts.map +1 -1
  51. package/dest/l1_artifacts.js +9 -24
  52. package/dest/l1_contract_addresses.d.ts +61 -65
  53. package/dest/l1_contract_addresses.d.ts.map +1 -1
  54. package/dest/l1_contract_addresses.js +101 -82
  55. package/dest/l1_reader.d.ts +5 -5
  56. package/dest/l1_reader.d.ts.map +1 -1
  57. package/dest/l1_reader.js +9 -7
  58. package/dest/l1_tx_utils/config.d.ts +7 -1
  59. package/dest/l1_tx_utils/config.d.ts.map +1 -1
  60. package/dest/l1_tx_utils/config.js +14 -1
  61. package/dest/l1_tx_utils/constants.d.ts +1 -1
  62. package/dest/l1_tx_utils/constants.js +2 -2
  63. package/dest/l1_tx_utils/factory.d.ts +18 -10
  64. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  65. package/dest/l1_tx_utils/factory.js +17 -7
  66. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +1 -1
  67. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +1 -1
  68. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts +15 -15
  69. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts.map +1 -1
  70. package/dest/l1_tx_utils/forwarder_l1_tx_utils.js +9 -15
  71. package/dest/l1_tx_utils/index-blobs.d.ts +3 -3
  72. package/dest/l1_tx_utils/index-blobs.d.ts.map +1 -1
  73. package/dest/l1_tx_utils/index-blobs.js +2 -2
  74. package/dest/l1_tx_utils/index.d.ts +2 -1
  75. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  76. package/dest/l1_tx_utils/index.js +1 -0
  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 +101 -66
  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 +13 -8
  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/publisher_manager.d.ts +21 -6
  87. package/dest/publisher_manager.d.ts.map +1 -1
  88. package/dest/publisher_manager.js +81 -7
  89. package/dest/queries.d.ts +13 -2
  90. package/dest/queries.d.ts.map +1 -1
  91. package/dest/queries.js +62 -3
  92. package/dest/test/chain_monitor.d.ts +23 -3
  93. package/dest/test/chain_monitor.d.ts.map +1 -1
  94. package/dest/test/chain_monitor.js +38 -2
  95. package/dest/test/eth_cheat_codes.d.ts +12 -4
  96. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  97. package/dest/test/eth_cheat_codes.js +17 -4
  98. package/dest/test/index.d.ts +1 -3
  99. package/dest/test/index.d.ts.map +1 -1
  100. package/dest/test/index.js +0 -2
  101. package/dest/test/rollup_cheat_codes.d.ts +33 -3
  102. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  103. package/dest/test/rollup_cheat_codes.js +68 -7
  104. package/dest/test/start_anvil.d.ts +23 -3
  105. package/dest/test/start_anvil.d.ts.map +1 -1
  106. package/dest/test/start_anvil.js +143 -29
  107. package/dest/test/upgrade_utils.js +2 -2
  108. package/dest/utils.d.ts +10 -2
  109. package/dest/utils.d.ts.map +1 -1
  110. package/dest/utils.js +94 -16
  111. package/package.json +6 -8
  112. package/src/client.ts +66 -2
  113. package/src/config.ts +24 -15
  114. package/src/contracts/chain_state_override.ts +200 -0
  115. package/src/contracts/fee_asset_handler.ts +2 -0
  116. package/src/contracts/fee_asset_price_oracle.ts +285 -0
  117. package/src/contracts/governance.ts +271 -12
  118. package/src/contracts/governance_proposer.ts +58 -2
  119. package/src/contracts/inbox.ts +13 -5
  120. package/src/contracts/index.ts +3 -2
  121. package/src/contracts/multicall.ts +243 -102
  122. package/src/contracts/outbox.ts +42 -8
  123. package/src/contracts/registry.ts +31 -1
  124. package/src/contracts/rollup.ts +443 -73
  125. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +16 -14
  126. package/src/deploy_aztec_l1_contracts.ts +69 -42
  127. package/src/deploy_l1_contract.ts +3 -3
  128. package/src/generated/l1-contracts-defaults.ts +2 -2
  129. package/src/l1_artifacts.ts +12 -35
  130. package/src/l1_contract_addresses.ts +120 -94
  131. package/src/l1_reader.ts +17 -10
  132. package/src/l1_tx_utils/config.ts +20 -0
  133. package/src/l1_tx_utils/constants.ts +2 -2
  134. package/src/l1_tx_utils/factory.ts +31 -31
  135. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +1 -1
  136. package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +1 -1
  137. package/src/l1_tx_utils/forwarder_l1_tx_utils.ts +43 -54
  138. package/src/l1_tx_utils/index-blobs.ts +2 -2
  139. package/src/l1_tx_utils/index.ts +1 -0
  140. package/src/l1_tx_utils/l1_tx_utils.ts +99 -48
  141. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +13 -8
  142. package/src/{test → l1_tx_utils}/tx_delayer.ts +93 -56
  143. package/src/publisher_manager.ts +107 -10
  144. package/src/queries.ts +70 -4
  145. package/src/test/chain_monitor.ts +65 -3
  146. package/src/test/eth_cheat_codes.ts +16 -4
  147. package/src/test/index.ts +0 -2
  148. package/src/test/rollup_cheat_codes.ts +73 -6
  149. package/src/test/start_anvil.ts +177 -29
  150. package/src/test/upgrade_utils.ts +2 -2
  151. package/src/utils.ts +98 -18
  152. package/dest/contracts/empire_slashing_proposer.d.ts +0 -67
  153. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  154. package/dest/contracts/empire_slashing_proposer.js +0 -209
  155. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  156. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +0 -26
  157. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +0 -1
  158. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +0 -26
  159. package/dest/test/delayed_tx_utils.d.ts +0 -13
  160. package/dest/test/delayed_tx_utils.d.ts.map +0 -1
  161. package/dest/test/delayed_tx_utils.js +0 -28
  162. package/dest/test/tx_delayer.d.ts +0 -36
  163. package/dest/test/tx_delayer.d.ts.map +0 -1
  164. package/src/contracts/empire_slashing_proposer.ts +0 -270
  165. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +0 -77
  166. package/src/test/delayed_tx_utils.ts +0 -52
@@ -1,6 +1,8 @@
1
1
  import { pick } from '@aztec/foundation/collection';
2
- import { createLogger } from '@aztec/foundation/log';
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,22 +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
- private log = createLogger('publisher:manager');
32
- private config: { publisherAllowInvalidStates?: boolean };
40
+ private log: Logger;
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 },
48
+ config: PublisherManagerConfig,
49
+ opts?: { bindings?: LoggerBindings; funder?: UtilsType },
37
50
  ) {
51
+ this.funder = opts?.funder;
52
+ this.log = createLogger('publisher:manager', opts?.bindings);
38
53
  this.log.info(`PublisherManager initialized with ${publishers.length} publishers.`);
39
54
  this.publishers = publishers;
40
- 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
+ }
41
70
  }
42
71
 
43
- /** Loads the state of all publishers and resumes monitoring any pending txs */
44
- public async loadState(): Promise<void> {
45
- 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();
46
98
  }
47
99
 
48
100
  // Finds and prioritises available publishers based on
@@ -100,7 +152,52 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
100
152
  return sortedPublishers[0].publisher;
101
153
  }
102
154
 
103
- public interrupt() {
104
- 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`);
105
202
  }
106
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
  }
package/src/test/index.ts CHANGED
@@ -1,8 +1,6 @@
1
- export * from './delayed_tx_utils.js';
2
1
  export * from './eth_cheat_codes.js';
3
2
  export * from './eth_cheat_codes_with_state.js';
4
3
  export * from './start_anvil.js';
5
- export * from './tx_delayer.js';
6
4
  export * from './upgrade_utils.js';
7
5
  export * from './chain_monitor.js';
8
6
  export * from './rollup_cheat_codes.js';
@@ -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,26 @@ 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)]);
68
+ }
69
+
70
+ /** Returns the number of seconds until the start of the given slot based on L1 block timestamp. */
71
+ public async getSecondsUntilSlot(slot: SlotNumber): Promise<number> {
72
+ const [currentTimestamp, targetTimestamp] = await Promise.all([
73
+ this.client.getBlock().then(b => BigInt(b.timestamp)),
74
+ this.rollup.read.getTimestampForSlot([BigInt(slot)]),
75
+ ]);
76
+ return Math.max(0, Number(targetTimestamp - currentTimestamp));
57
77
  }
58
78
 
59
79
  /** Returns the current epoch */
@@ -159,6 +179,21 @@ export class RollupCheatCodes {
159
179
  return [timestamp, nextSlot];
160
180
  }
161
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
+
162
197
  /**
163
198
  * Warps time in L1 equivalent to however many slots.
164
199
  * @param howMany - The number of slots to advance.
@@ -250,12 +285,14 @@ export class RollupCheatCodes {
250
285
  });
251
286
  }
252
287
 
253
- public insertOutbox(epoch: EpochNumber, outHash: bigint) {
288
+ public insertOutbox(epoch: EpochNumber, numCheckpointsInEpoch: number, outHash: bigint) {
254
289
  return this.ethCheatCodes.execWithPausedAnvil(async () => {
255
290
  const outboxAddress = await this.rollup.read.getOutbox();
256
- const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch);
291
+ const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch, numCheckpointsInEpoch);
257
292
  await this.ethCheatCodes.store(EthAddress.fromString(outboxAddress), epochRootSlot, outHash);
258
- 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
+ );
259
296
  });
260
297
  }
261
298
 
@@ -304,7 +341,8 @@ export class RollupCheatCodes {
304
341
  }
305
342
 
306
343
  /**
307
- * 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.
308
346
  * @param ethValue - The new proving cost per mana in ETH
309
347
  */
310
348
  public async setProvingCostPerMana(ethValue: bigint) {
@@ -314,8 +352,37 @@ export class RollupCheatCodes {
314
352
  chain: this.client.chain,
315
353
  gasLimit: 1000000n,
316
354
  });
317
- 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
+ }
318
363
  this.logger.warn(`Updated proving cost per mana to ${ethValue}`);
319
364
  });
320
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
+ }
321
388
  }