@aztec/ethereum 3.0.0-nightly.20251031 → 3.0.0-nightly.20251101

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.
@@ -1,4 +1,4 @@
1
- import { getKeys, merge, pick, times } from '@aztec/foundation/collection';
1
+ import { getKeys, median, merge, pick, times } from '@aztec/foundation/collection';
2
2
  import { createLogger } from '@aztec/foundation/log';
3
3
  import { makeBackoff, retry } from '@aztec/foundation/retry';
4
4
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
@@ -7,6 +7,7 @@ import { MethodNotFoundRpcError, MethodNotSupportedRpcError, decodeErrorResult,
7
7
  import { defaultL1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
8
8
  import { BLOCK_TIME_MS, LARGE_GAS_LIMIT, MIN_BLOB_REPLACEMENT_BUMP_PERCENTAGE, MIN_REPLACEMENT_BUMP_PERCENTAGE, WEI_CONST } from './constants.js';
9
9
  import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './utils.js';
10
+ const HISTORICAL_BLOCK_COUNT = 5;
10
11
  export class ReadOnlyL1TxUtils {
11
12
  client;
12
13
  logger;
@@ -35,21 +36,128 @@ export class ReadOnlyL1TxUtils {
35
36
  return this.client.getBlockNumber();
36
37
  }
37
38
  /**
39
+ * Analyzes pending transactions and recent fee history to determine a competitive priority fee.
40
+ * Falls back to network estimate if data is unavailable or fails.
41
+ * @param networkEstimateResult - Result from estimateMaxPriorityFeePerGas RPC call
42
+ * @param pendingBlockResult - Result from getBlock with pending tag RPC call
43
+ * @param feeHistoryResult - Result from getFeeHistory RPC call
44
+ * @returns A competitive priority fee based on pending txs and recent block history
45
+ */ getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult) {
46
+ const networkEstimate = networkEstimateResult.status === 'fulfilled' && typeof networkEstimateResult.value === 'bigint' ? networkEstimateResult.value : 0n;
47
+ let competitiveFee = networkEstimate;
48
+ if (pendingBlockResult.status === 'fulfilled' && pendingBlockResult.value !== null && pendingBlockResult.value.transactions && pendingBlockResult.value.transactions.length > 0) {
49
+ const pendingBlock = pendingBlockResult.value;
50
+ // Extract priority fees from pending transactions
51
+ const pendingFees = pendingBlock.transactions.map((tx)=>{
52
+ // Transaction can be just a hash string, so we need to check if it's an object
53
+ if (typeof tx === 'string') {
54
+ return 0n;
55
+ }
56
+ const fee = tx.maxPriorityFeePerGas || 0n;
57
+ // Debug: Log suspicious fees
58
+ if (fee > 100n * WEI_CONST) {
59
+ this.logger?.warn('Suspicious high priority fee in pending tx', {
60
+ txHash: tx.hash,
61
+ maxPriorityFeePerGas: formatGwei(fee),
62
+ maxFeePerGas: formatGwei(tx.maxFeePerGas || 0n),
63
+ maxFeePerBlobGas: tx.maxFeePerBlobGas ? formatGwei(tx.maxFeePerBlobGas) : 'N/A'
64
+ });
65
+ }
66
+ return fee;
67
+ }).filter((fee)=>fee > 0n);
68
+ if (pendingFees.length > 0) {
69
+ // Use 75th percentile of pending fees to be competitive
70
+ const sortedPendingFees = [
71
+ ...pendingFees
72
+ ].sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
73
+ const percentile75Index = Math.floor((sortedPendingFees.length - 1) * 0.75);
74
+ const pendingCompetitiveFee = sortedPendingFees[percentile75Index];
75
+ if (pendingCompetitiveFee > competitiveFee) {
76
+ competitiveFee = pendingCompetitiveFee;
77
+ }
78
+ this.logger?.debug('Analyzed pending transactions for competitive pricing', {
79
+ pendingTxCount: pendingFees.length,
80
+ pendingP75: formatGwei(pendingCompetitiveFee)
81
+ });
82
+ }
83
+ }
84
+ if (feeHistoryResult.status === 'fulfilled' && feeHistoryResult.value !== null && feeHistoryResult.value.reward && feeHistoryResult.value.reward.length > 0) {
85
+ const feeHistory = feeHistoryResult.value;
86
+ // Extract 75th percentile fees from each block
87
+ const percentile75Fees = feeHistory.reward.map((rewards)=>rewards[0] || 0n).filter((fee)=>fee > 0n);
88
+ if (percentile75Fees.length > 0) {
89
+ // Calculate median of the 75th percentile fees across blocks
90
+ const medianHistoricalFee = median(percentile75Fees) ?? 0n;
91
+ // Debug: Log suspicious fees from history
92
+ if (medianHistoricalFee > 100n * WEI_CONST) {
93
+ this.logger?.warn('Suspicious high fee in history', {
94
+ historicalMedian: formatGwei(medianHistoricalFee),
95
+ allP75Fees: percentile75Fees.map((f)=>formatGwei(f))
96
+ });
97
+ }
98
+ if (medianHistoricalFee > competitiveFee) {
99
+ competitiveFee = medianHistoricalFee;
100
+ }
101
+ this.logger?.debug('Analyzed fee history for competitive pricing', {
102
+ historicalMedian: formatGwei(medianHistoricalFee)
103
+ });
104
+ }
105
+ }
106
+ // Sanity check: cap competitive fee at 100x network estimate to avoid using unrealistic fees
107
+ // (e.g., Anvil returns inflated historical fees that don't reflect actual network conditions)
108
+ const maxReasonableFee = networkEstimate * 100n;
109
+ if (competitiveFee > maxReasonableFee) {
110
+ this.logger?.warn('Competitive fee exceeds sanity cap, using capped value', {
111
+ competitiveFee: formatGwei(competitiveFee),
112
+ networkEstimate: formatGwei(networkEstimate),
113
+ cappedTo: formatGwei(maxReasonableFee)
114
+ });
115
+ competitiveFee = maxReasonableFee;
116
+ }
117
+ // Log final decision
118
+ if (competitiveFee > networkEstimate) {
119
+ this.logger?.debug('Using competitive fee from market analysis', {
120
+ networkEstimate: formatGwei(networkEstimate),
121
+ competitive: formatGwei(competitiveFee)
122
+ });
123
+ }
124
+ return competitiveFee;
125
+ }
126
+ /**
38
127
  * Gets the current gas price with bounds checking
39
128
  */ async getGasPrice(gasConfigOverrides, isBlobTx = false, attempt = 0, previousGasPrice) {
40
129
  const gasConfig = merge(this.config, gasConfigOverrides);
41
- const block = await this.client.getBlock({
42
- blockTag: 'latest'
43
- });
44
- const baseFee = block.baseFeePerGas ?? 0n;
130
+ // Make all RPC calls in parallel upfront with retry logic
131
+ const latestBlockPromise = this.tryTwice(()=>this.client.getBlock({
132
+ blockTag: 'latest'
133
+ }), 'Getting latest block');
134
+ const networkEstimatePromise = gasConfig.fixedPriorityFeePerGas ? null : this.tryTwice(()=>this.client.estimateMaxPriorityFeePerGas(), 'Estimating max priority fee per gas');
135
+ const pendingBlockPromise = gasConfig.fixedPriorityFeePerGas ? null : this.tryTwice(()=>this.client.getBlock({
136
+ blockTag: 'pending',
137
+ includeTransactions: true
138
+ }), 'Getting pending block');
139
+ const feeHistoryPromise = gasConfig.fixedPriorityFeePerGas ? null : this.tryTwice(()=>this.client.getFeeHistory({
140
+ blockCount: HISTORICAL_BLOCK_COUNT,
141
+ rewardPercentiles: [
142
+ 75
143
+ ]
144
+ }), 'Getting fee history');
145
+ const blobBaseFeePromise = isBlobTx ? this.tryTwice(()=>this.client.getBlobBaseFee(), 'Getting blob base fee') : null;
146
+ const [latestBlockResult, networkEstimateResult, pendingBlockResult, feeHistoryResult, blobBaseFeeResult] = await Promise.allSettled([
147
+ latestBlockPromise,
148
+ networkEstimatePromise ?? Promise.resolve(0n),
149
+ pendingBlockPromise ?? Promise.resolve(null),
150
+ feeHistoryPromise ?? Promise.resolve(null),
151
+ blobBaseFeePromise ?? Promise.resolve(0n)
152
+ ]);
153
+ // Extract results
154
+ const baseFee = latestBlockResult.status === 'fulfilled' && typeof latestBlockResult.value === 'object' && latestBlockResult.value.baseFeePerGas ? latestBlockResult.value.baseFeePerGas : 0n;
45
155
  // Get blob base fee if available
46
156
  let blobBaseFee = 0n;
47
- if (isBlobTx) {
48
- try {
49
- blobBaseFee = await retry(()=>this.client.getBlobBaseFee(), 'Getting L1 blob base fee', makeBackoff(times(2, ()=>1)), this.logger, true);
50
- } catch {
51
- this.logger?.warn('Failed to get L1 blob base fee', attempt);
52
- }
157
+ if (isBlobTx && blobBaseFeeResult.status === 'fulfilled' && typeof blobBaseFeeResult.value === 'bigint') {
158
+ blobBaseFee = blobBaseFeeResult.value;
159
+ } else if (isBlobTx) {
160
+ this.logger?.warn('Failed to get L1 blob base fee', attempt);
53
161
  }
54
162
  let priorityFee;
55
163
  if (gasConfig.fixedPriorityFeePerGas) {
@@ -59,8 +167,8 @@ export class ReadOnlyL1TxUtils {
59
167
  // try to maintain precision up to 1000000 wei
60
168
  priorityFee = BigInt(gasConfig.fixedPriorityFeePerGas * 1_000_000) * (WEI_CONST / 1_000_000n);
61
169
  } else {
62
- // Get initial priority fee from the network
63
- priorityFee = await this.client.estimateMaxPriorityFeePerGas();
170
+ // Get competitive priority fee (includes network estimate + analysis)
171
+ priorityFee = this.getCompetitivePriorityFee(networkEstimateResult, pendingBlockResult, feeHistoryResult);
64
172
  }
65
173
  let maxFeePerGas = baseFee;
66
174
  let maxFeePerBlobGas = blobBaseFee;
@@ -81,26 +189,50 @@ export class ReadOnlyL1TxUtils {
81
189
  // multiply by 100 & divide by 100 to maintain some precision
82
190
  const minPriorityFee = previousGasPrice.maxPriorityFeePerGas * (100_00n + BigInt(bumpPercentage * 1_00)) / 100_00n;
83
191
  const minMaxFee = previousGasPrice.maxFeePerGas * (100_00n + BigInt(bumpPercentage * 1_00)) / 100_00n;
84
- // Add priority fee to maxFeePerGas
85
- maxFeePerGas += priorityFee;
86
- // Use maximum between current network values and minimum required values
87
- priorityFee = priorityFee > minPriorityFee ? priorityFee : minPriorityFee;
192
+ let competitivePriorityFee = priorityFee;
193
+ if (!gasConfig.fixedPriorityFeePerGas) {
194
+ // Apply bump percentage to competitive fee
195
+ competitivePriorityFee = priorityFee * (100_00n + BigInt(configBump * 1_00)) / 100_00n;
196
+ this.logger?.debug(`Speed-up attempt ${attempt}: using competitive fee strategy`, {
197
+ networkEstimate: formatGwei(priorityFee),
198
+ competitiveFee: formatGwei(competitivePriorityFee),
199
+ minRequired: formatGwei(minPriorityFee),
200
+ bumpPercentage: configBump
201
+ });
202
+ }
203
+ // Use maximum between competitive fee and minimum required bump
204
+ const finalPriorityFee = competitivePriorityFee > minPriorityFee ? competitivePriorityFee : minPriorityFee;
205
+ const feeSource = finalPriorityFee === competitivePriorityFee ? 'competitive' : 'minimum-bump';
206
+ priorityFee = finalPriorityFee;
207
+ // Add the final priority fee to maxFeePerGas
208
+ maxFeePerGas += finalPriorityFee;
88
209
  maxFeePerGas = maxFeePerGas > minMaxFee ? maxFeePerGas : minMaxFee;
210
+ if (!gasConfig.fixedPriorityFeePerGas) {
211
+ this.logger?.debug(`Speed-up fee decision: using ${feeSource} fee`, {
212
+ finalPriorityFee: formatGwei(finalPriorityFee)
213
+ });
214
+ }
89
215
  } else {
90
- // first attempt, just bump priority fee, unless it's a fixed config
216
+ // First attempt: apply configured bump percentage to competitive fee
91
217
  // multiply by 100 & divide by 100 to maintain some precision
92
218
  if (!gasConfig.fixedPriorityFeePerGas) {
93
219
  priorityFee = priorityFee * (100_00n + BigInt((gasConfig.priorityFeeBumpPercentage || 0) * 1_00)) / 100_00n;
220
+ this.logger?.debug('Initial transaction: using competitive fee from market analysis', {
221
+ networkEstimate: formatGwei(priorityFee)
222
+ });
94
223
  }
95
224
  maxFeePerGas += priorityFee;
96
225
  }
226
+ // maxGwei and maxBlobGwei are hard limits
227
+ const effectiveMaxGwei = gasConfig.maxGwei * WEI_CONST;
228
+ const effectiveMaxBlobGwei = gasConfig.maxBlobGwei * WEI_CONST;
97
229
  // Ensure we don't exceed maxGwei
98
- const maxGweiInWei = gasConfig.maxGwei * WEI_CONST;
99
- maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas;
230
+ if (effectiveMaxGwei > 0n) {
231
+ maxFeePerGas = maxFeePerGas > effectiveMaxGwei ? effectiveMaxGwei : maxFeePerGas;
232
+ }
100
233
  // Ensure we don't exceed maxBlobGwei
101
- if (maxFeePerBlobGas) {
102
- const maxBlobGweiInWei = gasConfig.maxBlobGwei * WEI_CONST;
103
- maxFeePerBlobGas = maxFeePerBlobGas > maxBlobGweiInWei ? maxBlobGweiInWei : maxFeePerBlobGas;
234
+ if (maxFeePerBlobGas && effectiveMaxBlobGwei > 0n) {
235
+ maxFeePerBlobGas = maxFeePerBlobGas > effectiveMaxBlobGwei ? effectiveMaxBlobGwei : maxFeePerBlobGas;
104
236
  }
105
237
  // Ensure priority fee doesn't exceed max fee
106
238
  const maxPriorityFeePerGas = priorityFee > maxFeePerGas ? maxFeePerGas : priorityFee;
@@ -291,4 +423,9 @@ export class ReadOnlyL1TxUtils {
291
423
  });
292
424
  return bumpedGasLimit;
293
425
  }
426
+ /**
427
+ * Helper function to retry RPC calls twice
428
+ */ tryTwice(fn, description) {
429
+ return retry(fn, description, makeBackoff(times(2, ()=>0)), this.logger, true);
430
+ }
294
431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/ethereum",
3
- "version": "3.0.0-nightly.20251031",
3
+ "version": "3.0.0-nightly.20251101",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -31,10 +31,10 @@
31
31
  "../package.common.json"
32
32
  ],
33
33
  "dependencies": {
34
- "@aztec/blob-lib": "3.0.0-nightly.20251031",
35
- "@aztec/constants": "3.0.0-nightly.20251031",
36
- "@aztec/foundation": "3.0.0-nightly.20251031",
37
- "@aztec/l1-artifacts": "3.0.0-nightly.20251031",
34
+ "@aztec/blob-lib": "3.0.0-nightly.20251101",
35
+ "@aztec/constants": "3.0.0-nightly.20251101",
36
+ "@aztec/foundation": "3.0.0-nightly.20251101",
37
+ "@aztec/l1-artifacts": "3.0.0-nightly.20251101",
38
38
  "@viem/anvil": "^0.0.10",
39
39
  "dotenv": "^16.0.3",
40
40
  "lodash.chunk": "^4.2.0",
package/src/config.ts CHANGED
@@ -156,6 +156,21 @@ const StagingIgnitionGovernanceConfiguration = {
156
156
  minimumVotes: 1250n * 200_000n * 10n ** 18n,
157
157
  };
158
158
 
159
+ const MainnetGovernanceConfiguration = {
160
+ proposeConfig: {
161
+ lockDelay: 90n * 24n * 60n * 60n,
162
+ lockAmount: 258_750_000n * 10n ** 18n,
163
+ },
164
+
165
+ votingDelay: 3n * 24n * 60n * 60n,
166
+ votingDuration: 7n * 24n * 60n * 60n,
167
+ executionDelay: 7n * 24n * 60n * 60n,
168
+ gracePeriod: 7n * 24n * 60n * 60n,
169
+ quorum: 2n * 10n ** 17n, // 20%
170
+ requiredYeaMargin: 33n * 10n ** 16n, // 33%
171
+ minimumVotes: 1000n * 200_000n * 10n ** 18n,
172
+ };
173
+
159
174
  export const getGovernanceConfiguration = (networkName: NetworkNames) => {
160
175
  switch (networkName) {
161
176
  case 'local':
@@ -170,6 +185,8 @@ export const getGovernanceConfiguration = (networkName: NetworkNames) => {
170
185
  return TestnetGovernanceConfiguration;
171
186
  case 'staging-ignition':
172
187
  return StagingIgnitionGovernanceConfiguration;
188
+ case 'mainnet':
189
+ return MainnetGovernanceConfiguration;
173
190
  default:
174
191
  throw new Error(`Unrecognized network name: ${networkName}`);
175
192
  }
@@ -186,6 +203,13 @@ const DefaultRewardConfig = {
186
203
  blockReward: 500n * 10n ** 18n,
187
204
  };
188
205
 
206
+ const MainnetRewardConfig = {
207
+ sequencerBps: 7_000,
208
+ rewardDistributor: EthAddress.ZERO.toString(),
209
+ booster: EthAddress.ZERO.toString(),
210
+ blockReward: 400n * 10n ** 18n,
211
+ };
212
+
189
213
  export const getRewardConfig = (networkName: NetworkNames) => {
190
214
  switch (networkName) {
191
215
  case 'local':
@@ -195,6 +219,8 @@ export const getRewardConfig = (networkName: NetworkNames) => {
195
219
  case 'testnet':
196
220
  case 'staging-ignition':
197
221
  return DefaultRewardConfig;
222
+ case 'mainnet':
223
+ return MainnetRewardConfig;
198
224
  default:
199
225
  throw new Error(`Unrecognized network name: ${networkName}`);
200
226
  }
@@ -204,11 +230,11 @@ export const getRewardBoostConfig = () => {
204
230
  // The reward configuration is specified with a precision of 1e5, and we use the same across
205
231
  // all networks.
206
232
  return {
207
- increment: 125000, // 1.25
208
- maxScore: 15000000, // 150
209
- a: 1000, // 0.01
210
- k: 1000000, // 10
211
- minimum: 100000, // 1
233
+ increment: 125_000, // 1.25
234
+ maxScore: 15_000_000, // 150
235
+ a: 1_000, // 0.01
236
+ k: 1_000_000, // 10
237
+ minimum: 100_000, // 1
212
238
  };
213
239
  };
214
240
 
@@ -245,6 +271,14 @@ const StagingIgnitionEntryQueueConfig = {
245
271
  maxQueueFlushSize: 24n,
246
272
  };
247
273
 
274
+ const MainnetEntryQueueConfig = {
275
+ bootstrapValidatorSetSize: 1_000n,
276
+ bootstrapFlushSize: 1_000n,
277
+ normalFlushSizeMin: 1n,
278
+ normalFlushSizeQuotient: 2_048n,
279
+ maxQueueFlushSize: 8n,
280
+ };
281
+
248
282
  export const getEntryQueueConfig = (networkName: NetworkNames) => {
249
283
  switch (networkName) {
250
284
  case 'local':
@@ -259,6 +293,8 @@ export const getEntryQueueConfig = (networkName: NetworkNames) => {
259
293
  return TestnetEntryQueueConfig;
260
294
  case 'staging-ignition':
261
295
  return StagingIgnitionEntryQueueConfig;
296
+ case 'mainnet':
297
+ return MainnetEntryQueueConfig;
262
298
  default:
263
299
  throw new Error(`Unrecognized network name: ${networkName}`);
264
300
  }
@@ -69,14 +69,14 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
69
69
  ...numberConfigHelper(20),
70
70
  },
71
71
  maxGwei: {
72
- description: 'Maximum gas price in gwei',
72
+ description: 'Maximum gas price in gwei to be used for transactions.',
73
73
  env: 'L1_GAS_PRICE_MAX',
74
- ...bigintConfigHelper(500n),
74
+ ...bigintConfigHelper(2000n),
75
75
  },
76
76
  maxBlobGwei: {
77
77
  description: 'Maximum blob fee per gas in gwei',
78
78
  env: 'L1_BLOB_FEE_PER_GAS_MAX',
79
- ...bigintConfigHelper(1_500n),
79
+ ...bigintConfigHelper(3000n),
80
80
  },
81
81
  priorityFeeBumpPercentage: {
82
82
  description: 'How much to increase priority fee by each attempt (percentage)',
@@ -106,7 +106,7 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
106
106
  stallTimeMs: {
107
107
  description: 'How long before considering tx stalled',
108
108
  env: 'L1_TX_MONITOR_STALL_TIME_MS',
109
- ...numberConfigHelper(24_000), // 24s, 2 ethereum slots
109
+ ...numberConfigHelper(12_000), // 12s, 1 ethereum slot
110
110
  },
111
111
  txTimeoutMs: {
112
112
  description: 'How long to wait for a tx to be mined before giving up. Set to 0 to disable.',