Skip to content

fix: convert maxFeePerGas/maxPriorityFeePerGas from tinybars to weibars#4902

Open
crypt0grapher wants to merge 3 commits intohiero-ledger:mainfrom
crypt0grapher:fix/max-fee-per-gas-weibar
Open

fix: convert maxFeePerGas/maxPriorityFeePerGas from tinybars to weibars#4902
crypt0grapher wants to merge 3 commits intohiero-ledger:mainfrom
crypt0grapher:fix/max-fee-per-gas-weibar

Conversation

@crypt0grapher
Copy link

@crypt0grapher crypt0grapher commented Feb 10, 2026

Description

This PR fixes two EIP-1559 fee field issues in transaction and block responses:

Fix 1: maxFeePerGas and maxPriorityFeePerGas are returned in raw tinybars for type-2 transactions, while effectiveGasPrice, gasPrice, and baseFeePerGas are correctly in weibars. This unit mismatch breaks downstream tooling — for example, Blockscout computes maxFeePerGas - baseFeePerGas and shows negative gas prices. The fix applies TINYBAR_TO_WEIBAR_COEF multiplication in createTransactionFromContractResult(), matching the existing gasPrice conversion pattern.

Fix 2: baseFeePerGas in block responses uses the current network gas price at query time instead of the gas price at the block's timestamp. This causes baseFeePerGas to drift across historical blocks depending on when they were first queried. The fix uses getGasPriceInWeibars(requestDetails, blockTimestamp) instead of gasPrice(requestDetails), matching the pattern already used in getBlockReceipts.

Related issue(s)

Fixes #4901

Testing Guide

  1. Run the targeted unit/integration tests:
    cd packages/relay && npx ts-mocha --recursive \
      './tests/lib/factories/transactionFactory.spec.ts' \
      './tests/lib/eth/eth_getTransactionByHash.spec.ts' \
      './tests/lib/eth/eth_getTransactionByBlockHashAndIndex.spec.ts' \
      './tests/lib/eth/eth_getTransactionByBlockNumberAndIndex.spec.ts' \
      './tests/lib/eth/eth_getBlockByHash.spec.ts' \
      './tests/lib/eth/eth_getBlockByNumber.spec.ts' \
      --exit
  2. Verify type-2 transaction assertions now expect weibar-scale values (e.g., 0xcf38224400 instead of 0x59)
  3. Verify block test URL assertions use timestamped network/fees?timestamp=<block_ts> endpoint
  4. (Optional) Run live validation against a test network:
    GOLIATH_RPC_URL=<rpc_url> GOLIATH_TEST_PRIVATE_KEY=<key> npx tsx scripts/validate-fee-caps.ts
    This sends a type-2 tx and asserts maxFeePerGas is in weibar range and consistent with effectiveGasPrice.

Changes from original design (optional)

N/A

Additional work needed (optional)

N/A

Checklist

  • I've assigned an assignee to this PR and related issue(s) (if applicable)
  • I've assigned a label to this PR and related issue(s) (if applicable)
  • I've assigned a milestone to this PR and related issue(s) (if applicable)
  • I've updated documentation (code comments, README, etc. if applicable)
  • I've done sufficient testing (unit, integration, etc.)

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>
@crypt0grapher crypt0grapher requested a review from a team as a code owner February 10, 2026 18:57
Copilot AI review requested due to automatic review settings February 10, 2026 18:57
@crypt0grapher crypt0grapher requested a review from a team as a code owner February 10, 2026 18:57
@crypt0grapher crypt0grapher requested a review from simzzz February 10, 2026 18:57
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 maxFeePerGas and maxPriorityFeePerGas from tinybars → weibars when building type-2 transactions from mirror-node contract results.
  • Compute block baseFeePerGas using network/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.

Comment on lines +148 to +159
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;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
// 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;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const ethGasPriceGwei = Number(ethGasPriceWei) / 1e9;
const ethGasPriceGweiStr = ethers.formatUnits(ethGasPriceWei, 'gwei');
const ethGasPriceGwei = Number(ethGasPriceGweiStr);

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +216
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');
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition 'avgPrice < 0' is always false.

Suggested change
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');

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@BartoszSolkaBD BartoszSolkaBD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you consider referencing just the function name here? That approach should require less maintenance while providing the same value.

Copy link
Contributor

@simzzz simzzz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@simzzz simzzz added this to the 0.76.0 milestone Feb 19, 2026
@simzzz simzzz added the bug Something isn't working label Feb 19, 2026
@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 15.00000% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ages/relay/src/lib/factories/transactionFactory.ts 0.00% 16 Missing ⚠️
...ib/services/ethService/blockService/blockWorker.ts 75.00% 1 Missing ⚠️

❌ 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.
❌ Your project check has failed because the head coverage (72.98%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

❗ There is a different number of reports uploaded between BASE (5062f1a) and HEAD (694a6ee). Click for more details.

HEAD has 24 uploads less than BASE
Flag BASE (5062f1a) HEAD (694a6ee)
config-service 1 0
relay 1 0
server 1 0
ws-server 1 0
21 1
@@             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     
Flag Coverage Δ
config-service ?
relay ?
server ?
ws-server ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ib/services/ethService/blockService/blockWorker.ts 66.66% <75.00%> (-32.17%) ⬇️
...ages/relay/src/lib/factories/transactionFactory.ts 27.14% <0.00%> (-72.86%) ⬇️

... and 80 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

maxFeePerGas and maxPriorityFeePerGas returned in tinybars instead of weibars for type 0x2 transactions

3 participants

Comments