fix(rpc): report base fee as the gas price of derived txs - #37
Merged
Conversation
0xNilesh
force-pushed
the
fix/negative-block-reward
branch
from
August 3, 2026 07:23
18238cb to
d4143b0
Compare
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Summary
Blocks on donut.push.network whose only content is derived transactions
(protocol-internal, not user-signed) render a negative block reward.
Derived txs are reconstructed for JSON-RPC from ABCI events, which do not carry fee data, so they are
rebuilt with
GasFeeCap = GasTipCap = 0. The generic EIP-1559 formulas then resolve their price to0in both the transaction object and the receipt, while the receipt still reports
gasUsed > 0.Any consumer that models every transaction as burning
base_fee × gas_usedtherefore reads such a blockas burning more than it collected. Blockscout computes a block's reward as:
which for a derived-only block is
0 − base_fee × gas_used→ negative.This PR reports the block base fee — the effective price of a transaction that adds no priority tip —
as a derived tx's
gasPriceand receipteffectiveGasPrice. That makes the arithmetic net to exactlyzero, which matches the chain's actual economics: a derived tx pays nothing to the proposer and burns
nothing, so it should move the block reward by nothing.
Chain economics are unchanged. This is a JSON-RPC reporting fix only — no consensus, no state, no
upgrade handler.
Evidence
Live donut RPC, block
0x1357085, tx0xc8b5ce78d703dbca59db6461510d1a30949f642f78de936cdf20a00d3eb84513:baseFeePerGas0x3b9aca00(1 gwei)gasPrice0x00x3b9aca00effectiveGasPrice0x00x3b9aca00gasUsed0x17fa4(98,212)−98,212 gwei0Root cause
Backend.parseDerivedTxFromAdditionalFields(rpc/backend/comet_to_eth.go) rebuilds a derived tx as aDynamicFeeTxwith zero fee caps, because the emitted events carry no fee fields. Downstream:NewRPCTransactionFromIncompleteMsgsetGasPrice: tx.GasPrice()→GasFeeCap→0.ReceiptsFromCometBlockusedEffectiveGasPrice(tx, baseFee)=min(tip + baseFee, feeCap)→0.Both are correct for a real transaction and wrong for a synthetic one that is exempt from fees.
Changes
rpc/types/utils.goDerivedTxGasPrice(tx, baseFee).NewRPCTransactionFromIncompleteMsgnow reports the base fee asgasPricefor mined derived txs.rpc/backend/comet_to_eth.goReceiptsFromCometBlockuses the same helper for derived txs, so the receipt'seffectiveGasPriceagrees with the servedgasPrice.rpc/types/utils_test.gogasPricecases (mined, unknown base fee, unmined, no aliasing of the caller'sbaseFee).rpc/backend/derived_gas_price_test.goeth_getTransactionByHash.CHANGELOG.mdFallback behaviour: when the base fee is unavailable (pruned node) or the tx is not yet mined, the
reconstructed tx's own price is returned rather than inventing one — i.e. the previous behaviour.
Affected endpoints — all three tx-serving paths already threaded
baseFeethrough, so no signaturechanges were needed:
eth_getTransactionByHasheth_getTransactionByBlockHashAndIndex/eth_getTransactionByBlockNumberAndIndexeth_getBlockByNumber/eth_getBlockByHash(withfullTx = true)eth_getTransactionReceipt,eth_getBlockReceiptsDeliberately out of scope
parseDerivedTxFromAdditionalFieldsstill reconstructs with zero fee caps. Setting a non-zeroGasFeeCapthere would fix both fields in one place, but that same struct is fed to the tracers(
rpc/backend/tracing.go), where a non-zero price would deduct fees from the sender during trace replayand diverge from actual execution. This PR changes the reporting layer only.
maxFeePerGas/maxPriorityFeePerGasremain absent on derived txs, as before. They areuser-declared caps and a protocol-internal tx has none; fabricating them would invent data. Consumers
fall back to
gasPrice, which is now correct.Receipt consistency.
5ee581effixed only the transaction object and left the receipt at0. ThisPR fixes both, because recent Blockscout versions override the stored
gas_pricewith the receipt'seffectiveGasPricewhen it is present — leaving them inconsistent would make the fix depend on theindexer version. It is also simply correct for the two fields to agree.
Testing
Both new tests were verified to fail with the fix reverted and pass with it applied.
Risk and compatibility
DerivedTxGasPriceis additive.additional != nil). Standard user-signedtransactions take the unchanged code path.
eth_feeHistoryis unaffected: it iterates decodedMsgEthereumTxonly, so derived txs never enterits reward percentiles.
Deployment notes
including historical ones.
gas_price = 0for derived txs it indexed before this fix.Affected blocks must be re-indexed for their displayed rewards to correct.
Author Checklist
All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.
I have...
mainbranch