fix: convert maxFeePerGas/maxPriorityFeePerGas from tinybars to weibars#4902
fix: convert maxFeePerGas/maxPriorityFeePerGas from tinybars to weibars#4902crypt0grapher wants to merge 3 commits intohiero-ledger:mainfrom
Conversation
Apply TINYBAR_TO_WEIBAR_COEF multiplication to EIP-1559 fee cap fields in createTransactionFromContractResult(), matching the existing gasPrice conversion pattern. Previously these fields were passed through as raw tinybars, causing unit mismatch with effectiveGasPrice and baseFeePerGas. Fixes hiero-ledger#4901 Signed-off-by: crypt0grapher <7blockchains@gmail.com>
baseFeePerGas in block responses was using the current network gas price at query time rather than the gas price at the block's timestamp. This caused baseFeePerGas to drift across blocks depending on when they were first queried, and resulted in inconsistent values in Blockscout's gas price oracle. Use getGasPriceInWeibars(requestDetails, blockTimestamp) to fetch the fee schedule at block creation time, matching the pattern already used in getBlockReceipts. Signed-off-by: crypt0grapher <7blockchains@gmail.com>
Add Step 5 to validate-fee-caps.ts that queries the Blockscout stats API and asserts gas_prices are within 2x of eth_gasPrice and positive. Signed-off-by: crypt0grapher <7blockchains@gmail.com>
There was a problem hiding this comment.
Pull request overview
Fixes inconsistent fee-unit handling in the Relay’s Ethereum JSON-RPC responses by converting EIP-1559 fee caps to weibars and ensuring block baseFeePerGas is computed using the block’s timestamp (not query-time).
Changes:
- Convert
maxFeePerGasandmaxPriorityFeePerGasfrom tinybars → weibars when building type-2 transactions from mirror-node contract results. - Compute block
baseFeePerGasusingnetwork/fees?timestamp=<blockTime>to avoid historical drift. - Update/extend unit tests to assert the new weibar-valued fields and timestamped fees requests; add a live validation script.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/validate-fee-caps.ts | Adds a live EIP-1559 fee-cap validation script against a testnet RPC + Blockscout stats. |
| packages/relay/src/lib/factories/transactionFactory.ts | Converts EIP-1559 fee caps from tinybars to weibars when formatting tx objects. |
| packages/relay/src/lib/services/ethService/blockService/blockWorker.ts | Uses block-time gas price for baseFeePerGas instead of current-time gas price. |
| packages/relay/tests/lib/factories/transactionFactory.spec.ts | Updates expected maxFeePerGas value to reflect weibar conversion. |
| packages/relay/tests/lib/eth/eth_getTransactionByHash.spec.ts | Updates assertions for fee caps to weibar values. |
| packages/relay/tests/lib/eth/eth_getTransactionByBlockNumberAndIndex.spec.ts | Adds/updates assertions ensuring type-2 fee caps are converted. |
| packages/relay/tests/lib/eth/eth_getTransactionByBlockHashAndIndex.spec.ts | Adds assertions ensuring type-2 fee caps are converted. |
| packages/relay/tests/lib/eth/eth_getBlockByNumber.spec.ts | Updates mocks/assertions for timestamped network/fees requests. |
| packages/relay/tests/lib/eth/eth_getBlockByHash.spec.ts | Updates mocks for timestamped network/fees requests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const ratio = | ||
| result.maxFeePerGasDec > result.effectiveGasPriceDec | ||
| ? result.maxFeePerGasDec / result.effectiveGasPriceDec | ||
| : result.effectiveGasPriceDec / result.maxFeePerGasDec; | ||
|
|
||
| console.log(` maxFeePerGas / effectiveGasPrice ratio: ${ratio}x`); | ||
|
|
||
| if (ratio > 100n) { | ||
| result.errors.push( | ||
| `maxFeePerGas (${result.maxFeePerGasDec}) and effectiveGasPrice (${result.effectiveGasPriceDec}) differ by ${ratio}x — unit mismatch detected.`, | ||
| ); | ||
| result.passed = false; |
There was a problem hiding this comment.
The ratio calculation can throw a division-by-zero error when maxFeePerGasDec is 0 (e.g., if the RPC returns 0x0 or the field is missing/malformed). Add a guard to skip the ratio check (or record an error) when maxFeePerGasDec is 0n before doing effectiveGasPriceDec / maxFeePerGasDec.
| const ratio = | |
| result.maxFeePerGasDec > result.effectiveGasPriceDec | |
| ? result.maxFeePerGasDec / result.effectiveGasPriceDec | |
| : result.effectiveGasPriceDec / result.maxFeePerGasDec; | |
| console.log(` maxFeePerGas / effectiveGasPrice ratio: ${ratio}x`); | |
| if (ratio > 100n) { | |
| result.errors.push( | |
| `maxFeePerGas (${result.maxFeePerGasDec}) and effectiveGasPrice (${result.effectiveGasPriceDec}) differ by ${ratio}x — unit mismatch detected.`, | |
| ); | |
| result.passed = false; | |
| if (result.maxFeePerGasDec === 0n) { | |
| result.errors.push( | |
| 'maxFeePerGas is 0, cannot compute ratio against effectiveGasPrice — possible malformed or zero-valued RPC response.', | |
| ); | |
| result.passed = false; | |
| } else { | |
| const ratio = | |
| result.maxFeePerGasDec > result.effectiveGasPriceDec | |
| ? result.maxFeePerGasDec / result.effectiveGasPriceDec | |
| : result.effectiveGasPriceDec / result.maxFeePerGasDec; | |
| console.log(` maxFeePerGas / effectiveGasPrice ratio: ${ratio}x`); | |
| if (ratio > 100n) { | |
| result.errors.push( | |
| `maxFeePerGas (${result.maxFeePerGasDec}) and effectiveGasPrice (${result.effectiveGasPriceDec}) differ by ${ratio}x — unit mismatch detected.`, | |
| ); | |
| result.passed = false; | |
| } |
| // Compare with eth_gasPrice (convert from wei to Gwei) | ||
| const ethGasPriceResult = await provider.send('eth_gasPrice', []); | ||
| const ethGasPriceWei = BigInt(ethGasPriceResult); | ||
| const ethGasPriceGwei = Number(ethGasPriceWei) / 1e9; |
There was a problem hiding this comment.
Converting ethGasPriceWei from BigInt to Number (Number(ethGasPriceWei) / 1e9) can lose precision or overflow for larger gas prices, which makes the Blockscout comparison unreliable. Prefer computing/formatting in bigint (e.g., ethers.formatUnits(ethGasPriceWei, 'gwei')) and keep comparisons in a safe numeric domain.
| const ethGasPriceGwei = Number(ethGasPriceWei) / 1e9; | |
| const ethGasPriceGweiStr = ethers.formatUnits(ethGasPriceWei, 'gwei'); | |
| const ethGasPriceGwei = Number(ethGasPriceGweiStr); |
| if (avgPrice != null && avgPrice > 0) { | ||
| // Compare with eth_gasPrice (convert from wei to Gwei) | ||
| const ethGasPriceResult = await provider.send('eth_gasPrice', []); | ||
| const ethGasPriceWei = BigInt(ethGasPriceResult); | ||
| const ethGasPriceGwei = Number(ethGasPriceWei) / 1e9; | ||
|
|
||
| console.log(` eth_gasPrice: ${ethGasPriceGwei.toFixed(2)} Gwei`); | ||
|
|
||
| const blockscoutRatio = avgPrice > ethGasPriceGwei ? avgPrice / ethGasPriceGwei : ethGasPriceGwei / avgPrice; | ||
| console.log(` Blockscout/eth_gasPrice ratio: ${blockscoutRatio.toFixed(2)}x`); | ||
|
|
||
| // Assertion 6: Blockscout gas_prices must be within 2x of eth_gasPrice | ||
| if (blockscoutRatio > 2) { | ||
| result.errors.push( | ||
| `Blockscout gas_prices.average (${avgPrice} Gwei) differs from eth_gasPrice (${ethGasPriceGwei.toFixed(2)} Gwei) by ${blockscoutRatio.toFixed(2)}x — expected within 2x.`, | ||
| ); | ||
| result.passed = false; | ||
| } | ||
|
|
||
| // Assertion 7: Blockscout gas_prices must be positive (not negative from unit mismatch) | ||
| if (avgPrice < 0) { | ||
| result.errors.push(`Blockscout gas_prices.average is negative (${avgPrice} Gwei) — unit mismatch issue.`); | ||
| result.passed = false; | ||
| } | ||
| } else { | ||
| console.log(' WARNING: Blockscout gas_prices.average is null/0 — skipping comparison'); |
There was a problem hiding this comment.
The condition 'avgPrice < 0' is always false.
| if (avgPrice != null && avgPrice > 0) { | |
| // Compare with eth_gasPrice (convert from wei to Gwei) | |
| const ethGasPriceResult = await provider.send('eth_gasPrice', []); | |
| const ethGasPriceWei = BigInt(ethGasPriceResult); | |
| const ethGasPriceGwei = Number(ethGasPriceWei) / 1e9; | |
| console.log(` eth_gasPrice: ${ethGasPriceGwei.toFixed(2)} Gwei`); | |
| const blockscoutRatio = avgPrice > ethGasPriceGwei ? avgPrice / ethGasPriceGwei : ethGasPriceGwei / avgPrice; | |
| console.log(` Blockscout/eth_gasPrice ratio: ${blockscoutRatio.toFixed(2)}x`); | |
| // Assertion 6: Blockscout gas_prices must be within 2x of eth_gasPrice | |
| if (blockscoutRatio > 2) { | |
| result.errors.push( | |
| `Blockscout gas_prices.average (${avgPrice} Gwei) differs from eth_gasPrice (${ethGasPriceGwei.toFixed(2)} Gwei) by ${blockscoutRatio.toFixed(2)}x — expected within 2x.`, | |
| ); | |
| result.passed = false; | |
| } | |
| // Assertion 7: Blockscout gas_prices must be positive (not negative from unit mismatch) | |
| if (avgPrice < 0) { | |
| result.errors.push(`Blockscout gas_prices.average is negative (${avgPrice} Gwei) — unit mismatch issue.`); | |
| result.passed = false; | |
| } | |
| } else { | |
| console.log(' WARNING: Blockscout gas_prices.average is null/0 — skipping comparison'); | |
| if (avgPrice != null) { | |
| // Assertion 7: Blockscout gas_prices must be positive (not negative from unit mismatch) | |
| if (avgPrice < 0) { | |
| result.errors.push(`Blockscout gas_prices.average is negative (${avgPrice} Gwei) — unit mismatch issue.`); | |
| result.passed = false; | |
| } else if (avgPrice === 0) { | |
| console.log(' WARNING: Blockscout gas_prices.average is 0 — skipping comparison'); | |
| } else { | |
| // Compare with eth_gasPrice (convert from wei to Gwei) | |
| const ethGasPriceResult = await provider.send('eth_gasPrice', []); | |
| const ethGasPriceWei = BigInt(ethGasPriceResult); | |
| const ethGasPriceGwei = Number(ethGasPriceWei) / 1e9; | |
| console.log(` eth_gasPrice: ${ethGasPriceGwei.toFixed(2)} Gwei`); | |
| const blockscoutRatio = | |
| avgPrice > ethGasPriceGwei ? avgPrice / ethGasPriceGwei : ethGasPriceGwei / avgPrice; | |
| console.log(` Blockscout/eth_gasPrice ratio: ${blockscoutRatio.toFixed(2)}x`); | |
| // Assertion 6: Blockscout gas_prices must be within 2x of eth_gasPrice | |
| if (blockscoutRatio > 2) { | |
| result.errors.push( | |
| `Blockscout gas_prices.average (${avgPrice} Gwei) differs from eth_gasPrice (${ethGasPriceGwei.toFixed(2)} Gwei) by ${blockscoutRatio.toFixed(2)}x — expected within 2x.`, | |
| ); | |
| result.passed = false; | |
| } | |
| } | |
| } else { | |
| console.log(' WARNING: Blockscout gas_prices.average is null — skipping comparison'); |
BartoszSolkaBD
left a comment
There was a problem hiding this comment.
Would you mind also updating the tests in packages/relay/tests/lib/openrpc.spec.ts and removing scripts/validate-fee-caps.ts? The automated test suite should be sufficient to cover these changes.
|
|
||
| const gasPrice = await commonService.gasPrice(requestDetails); | ||
| // Use block-time gas price (not current) so baseFeePerGas matches effectiveGasPrice. | ||
| // Mirrors the pattern in getBlockReceipts (line 364). |
There was a problem hiding this comment.
Could you consider referencing just the function name here? That approach should require less maintenance while providing the same value.
simzzz
left a comment
There was a problem hiding this comment.
Looks good overall, +1 for removing scripts/validate-fee-caps.ts, you also have some conflicts, could you fix those as well please? Thank you
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (15.00%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.
@@ Coverage Diff @@
## main #4902 +/- ##
===========================================
- Coverage 96.02% 72.98% -23.04%
===========================================
Files 143 143
Lines 23342 23359 +17
Branches 1853 762 -1091
===========================================
- Hits 22413 17049 -5364
- Misses 905 6276 +5371
- Partials 24 34 +10
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 80 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Description
This PR fixes two EIP-1559 fee field issues in transaction and block responses:
Fix 1:
maxFeePerGasandmaxPriorityFeePerGasare returned in raw tinybars for type-2 transactions, whileeffectiveGasPrice,gasPrice, andbaseFeePerGasare correctly in weibars. This unit mismatch breaks downstream tooling — for example, Blockscout computesmaxFeePerGas - baseFeePerGasand shows negative gas prices. The fix appliesTINYBAR_TO_WEIBAR_COEFmultiplication increateTransactionFromContractResult(), matching the existinggasPriceconversion pattern.Fix 2:
baseFeePerGasin block responses uses the current network gas price at query time instead of the gas price at the block's timestamp. This causesbaseFeePerGasto drift across historical blocks depending on when they were first queried. The fix usesgetGasPriceInWeibars(requestDetails, blockTimestamp)instead ofgasPrice(requestDetails), matching the pattern already used ingetBlockReceipts.Related issue(s)
Fixes #4901
Testing Guide
0xcf38224400instead of0x59)network/fees?timestamp=<block_ts>endpointmaxFeePerGasis in weibar range and consistent witheffectiveGasPrice.Changes from original design (optional)
N/A
Additional work needed (optional)
N/A
Checklist