diff --git a/CHANGELOG.md b/CHANGELOG.md index c3de89b06c..9ded010cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (rpc) [#1688](https://github.com/evmos/ethermint/pull/1688) Align filter rule for `debug_traceBlockByNumber` * (rpc) [#1722](https://github.com/evmos/ethermint/pull/1722) Align revert response for `eth_estimateGas` and `eth_call` as Ethereum. +* (rpc) [#1720](https://github.com/evmos/ethermint/pull/1720) Fix next block fee for historical block and calculate base fee by params. ### Improvements diff --git a/rpc/backend/chain_info.go b/rpc/backend/chain_info.go index bd78e1d1d6..f7c9297a95 100644 --- a/rpc/backend/chain_info.go +++ b/rpc/backend/chain_info.go @@ -17,8 +17,10 @@ package backend import ( "fmt" + "math" "math/big" "strconv" + "sync" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common/hexutil" @@ -153,20 +155,34 @@ func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { return address, nil } +var ( + errInvalidPercentile = fmt.Errorf("invalid reward percentile") + errRequestBeyondHead = fmt.Errorf("request beyond head block") +) + // FeeHistory returns data relevant for fee estimation based on the specified range of blocks. func (b *Backend) FeeHistory( userBlockCount rpc.DecimalOrHex, // number blocks to fetch, maximum is 100 lastBlock rpc.BlockNumber, // the block to start search , to oldest rewardPercentiles []float64, // percentiles to fetch reward ) (*rpctypes.FeeHistoryResult, error) { + for i, p := range rewardPercentiles { + if p < 0 || p > 100 { + return nil, fmt.Errorf("%w: %f", errInvalidPercentile, p) + } + if i > 0 && p < rewardPercentiles[i-1] { + return nil, fmt.Errorf("%w: #%d:%f > #%d:%f", errInvalidPercentile, i-1, rewardPercentiles[i-1], i, p) + } + } + blockNumber, err := b.BlockNumber() + if err != nil { + return nil, err + } blockEnd := int64(lastBlock) - if blockEnd < 0 { - blockNumber, err := b.BlockNumber() - if err != nil { - return nil, err - } blockEnd = int64(blockNumber) + } else if int64(blockNumber) < blockEnd { + return nil, fmt.Errorf("%w: requested %d, head %d", errRequestBeyondHead, blockEnd, int64(blockNumber)) } blocks := int64(userBlockCount) @@ -174,8 +190,7 @@ func (b *Backend) FeeHistory( if blocks > maxBlockCount { return nil, fmt.Errorf("FeeHistory user block count %d higher than %d", blocks, maxBlockCount) } - - if blockEnd+1 < blocks { + if blockEnd < math.MaxInt64 && blockEnd+1 < blocks { blocks = blockEnd + 1 } // Ensure not trying to retrieve before genesis. @@ -194,46 +209,71 @@ func (b *Backend) FeeHistory( // rewards should only be calculated if reward percentiles were included calculateRewards := rewardCount != 0 + const maxBlockFetchers = 4 + for blockID := blockStart; blockID <= blockEnd; blockID += maxBlockFetchers { + wg := sync.WaitGroup{} + wgDone := make(chan bool) + chanErr := make(chan error) + for i := 0; i < maxBlockFetchers; i++ { + if blockID+int64(i) >= blockEnd+1 { + break + } + wg.Add(1) + go func(index int32) { + defer wg.Done() + // fetch block + // tendermint block + blockNum := rpctypes.BlockNumber(blockStart + int64(index)) + tendermintblock, err := b.TendermintBlockByNumber(blockNum) + if tendermintblock == nil { + chanErr <- err + return + } - // fetch block - for blockID := blockStart; blockID <= blockEnd; blockID++ { - index := int32(blockID - blockStart) - // tendermint block - tendermintblock, err := b.TendermintBlockByNumber(rpctypes.BlockNumber(blockID)) - if tendermintblock == nil { - return nil, err - } - - // eth block - ethBlock, err := b.GetBlockByNumber(rpctypes.BlockNumber(blockID), true) - if ethBlock == nil { - return nil, err - } + // eth block + ethBlock, err := b.GetBlockByNumber(blockNum, true) + if ethBlock == nil { + chanErr <- err + return + } - // tendermint block result - tendermintBlockResult, err := b.TendermintBlockResultByNumber(&tendermintblock.Block.Height) - if tendermintBlockResult == nil { - b.logger.Debug("block result not found", "height", tendermintblock.Block.Height, "error", err.Error()) - return nil, err - } + // tendermint block result + tendermintBlockResult, err := b.TendermintBlockResultByNumber(&tendermintblock.Block.Height) + if tendermintBlockResult == nil { + b.logger.Debug("block result not found", "height", tendermintblock.Block.Height, "error", err.Error()) + chanErr <- err + return + } - oneFeeHistory := rpctypes.OneFeeHistory{} - err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) - if err != nil { - return nil, err - } + oneFeeHistory := rpctypes.OneFeeHistory{} + err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) + if err != nil { + chanErr <- err + return + } - // copy - thisBaseFee[index] = (*hexutil.Big)(oneFeeHistory.BaseFee) - thisBaseFee[index+1] = (*hexutil.Big)(oneFeeHistory.NextBaseFee) - thisGasUsedRatio[index] = oneFeeHistory.GasUsedRatio - if calculateRewards { - for j := 0; j < rewardCount; j++ { - reward[index][j] = (*hexutil.Big)(oneFeeHistory.Reward[j]) - if reward[index][j] == nil { - reward[index][j] = (*hexutil.Big)(big.NewInt(0)) + // copy + thisBaseFee[index] = (*hexutil.Big)(oneFeeHistory.BaseFee) + thisBaseFee[index+1] = (*hexutil.Big)(oneFeeHistory.NextBaseFee) + thisGasUsedRatio[index] = oneFeeHistory.GasUsedRatio + if calculateRewards { + for j := 0; j < rewardCount; j++ { + reward[index][j] = (*hexutil.Big)(oneFeeHistory.Reward[j]) + if reward[index][j] == nil { + reward[index][j] = (*hexutil.Big)(big.NewInt(0)) + } + } } - } + }(int32(blockID - blockStart + int64(i))) + } + go func() { + wg.Wait() + close(wgDone) + }() + select { + case <-wgDone: + case err := <-chanErr: + return nil, err } } diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index 629747c8e9..7a21cf72dc 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -30,8 +30,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -39,6 +40,7 @@ import ( "github.com/evmos/ethermint/rpc/types" evmtypes "github.com/evmos/ethermint/x/evm/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" "github.com/tendermint/tendermint/proto/tendermint/crypto" ) @@ -117,6 +119,47 @@ func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height i return nonce, nil } +// CalcBaseFee calculates the basefee of the header. +func CalcBaseFee(config *params.ChainConfig, parent *ethtypes.Header, baseFeeChangeDenominator, elasticityMultiplier uint32) *big.Int { + // If the current block is the first EIP-1559 block, return the InitialBaseFee. + if !config.IsLondon(parent.Number) { + return new(big.Int).SetUint64(params.InitialBaseFee) + } + + parentGasTarget := parent.GasLimit / uint64(elasticityMultiplier) + // If the parent gasUsed is the same as the target, the baseFee remains unchanged. + if parent.GasUsed == parentGasTarget { + return new(big.Int).Set(parent.BaseFee) + } + + var ( + num = new(big.Int) + denom = new(big.Int) + ) + + if parent.GasUsed > parentGasTarget { + // If the parent block used more gas than its target, the baseFee should increase. + // max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) + num.SetUint64(parent.GasUsed - parentGasTarget) + num.Mul(num, parent.BaseFee) + num.Div(num, denom.SetUint64(parentGasTarget)) + num.Div(num, denom.SetUint64(uint64(baseFeeChangeDenominator))) + baseFeeDelta := math.BigMax(num, common.Big1) + + return num.Add(parent.BaseFee, baseFeeDelta) + } else { + // Otherwise if the parent block used less gas than its target, the baseFee should decrease. + // max(0, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) + num.SetUint64(parentGasTarget - parent.GasUsed) + num.Mul(num, parent.BaseFee) + num.Div(num, denom.SetUint64(parentGasTarget)) + num.Div(num, denom.SetUint64(uint64(baseFeeChangeDenominator))) + baseFee := num.Sub(parent.BaseFee, num) + + return math.BigMax(baseFee, common.Big0) + } +} + // output: targetOneFeeHistory func (b *Backend) processBlock( tendermintBlock *tmrpctypes.ResultBlock, @@ -134,11 +177,6 @@ func (b *Backend) processBlock( // set basefee targetOneFeeHistory.BaseFee = blockBaseFee cfg := b.ChainConfig() - if cfg.IsLondon(big.NewInt(blockHeight + 1)) { - targetOneFeeHistory.NextBaseFee = misc.CalcBaseFee(cfg, b.CurrentHeader()) - } else { - targetOneFeeHistory.NextBaseFee = new(big.Int) - } // set gas used ratio gasLimitUint64, ok := (*ethBlock)["gasLimit"].(hexutil.Uint64) if !ok { @@ -150,6 +188,26 @@ func (b *Backend) processBlock( return fmt.Errorf("invalid gas used type: %T", (*ethBlock)["gasUsed"]) } + baseFee, ok := (*ethBlock)["baseFeePerGas"].(*hexutil.Big) + if !ok { + return fmt.Errorf("invalid baseFee: %T", (*ethBlock)["baseFeePerGas"]) + } + + if cfg.IsLondon(big.NewInt(blockHeight + 1)) { + var header ethtypes.Header + header.Number = new(big.Int).SetInt64(blockHeight) + header.BaseFee = baseFee.ToInt() + header.GasLimit = uint64(gasLimitUint64) + header.GasUsed = gasUsedBig.ToInt().Uint64() + params, err := b.queryClient.FeeMarket.Params(b.ctx, &feemarkettypes.QueryParamsRequest{}) + if err != nil { + return err + } + targetOneFeeHistory.NextBaseFee = CalcBaseFee(cfg, &header, params.Params.BaseFeeChangeDenominator, params.Params.ElasticityMultiplier) + } else { + targetOneFeeHistory.NextBaseFee = new(big.Int) + } + gasusedfloat, _ := new(big.Float).SetInt(gasUsedBig.ToInt()).Float64() if gasLimitUint64 <= 0 { diff --git a/tests/integration_tests/configs/fee-history.jsonnet b/tests/integration_tests/configs/fee-history.jsonnet new file mode 100644 index 0000000000..0e631df173 --- /dev/null +++ b/tests/integration_tests/configs/fee-history.jsonnet @@ -0,0 +1,16 @@ +local config = import 'default.jsonnet'; + +config { + 'ethermint_9000-1'+: { + genesis+: { + app_state+: { + feemarket+: { + params+: { + elasticity_multiplier: 3, + base_fee_change_denominator: 100000000, + }, + }, + }, + }, + }, +} diff --git a/tests/integration_tests/cosmoscli.py b/tests/integration_tests/cosmoscli.py index 41c24c91e7..8e934a8ab3 100644 --- a/tests/integration_tests/cosmoscli.py +++ b/tests/integration_tests/cosmoscli.py @@ -273,9 +273,9 @@ def validators(self): ) )["validators"] - def staking_params(self): + def get_params(self, module): return json.loads( - self.raw("query", "staking", "params", output="json", node=self.node_rpc) + self.raw("query", module, "params", output="json", node=self.node_rpc) ) def staking_pool(self, bonded=True): diff --git a/tests/integration_tests/test_fee_history.py b/tests/integration_tests/test_fee_history.py index 7dde9c187a..a6fa0aa10e 100644 --- a/tests/integration_tests/test_fee_history.py +++ b/tests/integration_tests/test_fee_history.py @@ -1,16 +1,19 @@ from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path import pytest from web3 import Web3 -from .network import setup_ethermint -from .utils import ADDRS, send_transaction +from .network import setup_custom_ethermint +from .utils import ADDRS, send_transaction, w3_wait_for_new_blocks @pytest.fixture(scope="module") def custom_ethermint(tmp_path_factory): path = tmp_path_factory.mktemp("fee-history") - yield from setup_ethermint(path, 26500, long_timeout_commit=True) + yield from setup_custom_ethermint( + path, 26500, Path(__file__).parent / "configs/fee-history.jsonnet" + ) @pytest.fixture(scope="module", params=["ethermint", "geth"]) @@ -71,3 +74,89 @@ def test_basic(cluster): assert len(res[field]) == target oldest = i + min - max assert res["oldestBlock"] == hex(oldest if oldest > 0 else 0) + + +def test_change(cluster): + w3: Web3 = cluster.w3 + call = w3.provider.make_request + tx = {"to": ADDRS["community"], "value": 10, "gasPrice": w3.eth.gas_price} + send_transaction(w3, tx) + size = 4 + method = "eth_feeHistory" + field = "baseFeePerGas" + percentiles = [100] + for b in ["latest", hex(w3.eth.block_number)]: + history0 = call(method, [size, b, percentiles])["result"][field] + w3_wait_for_new_blocks(w3, 2, 0.1) + history1 = call(method, [size, b, percentiles])["result"][field] + if b == "latest": + assert history1 != history0 + else: + assert history1 == history0 + + +def adjust_base_fee(parent_fee, gas_limit, gas_used, denominator, multiplier): + "spec: https://eips.ethereum.org/EIPS/eip-1559#specification" + gas_target = gas_limit // multiplier + delta = parent_fee * (gas_target - gas_used) // gas_target // denominator + return parent_fee - delta + + +def test_next(cluster, custom_ethermint): + w3: Web3 = cluster.w3 + # geth default + elasticity_multiplier = 2 + change_denominator = 8 + if cluster == custom_ethermint: + params = cluster.cosmos_cli().get_params("feemarket")["params"] + elasticity_multiplier = params["elasticity_multiplier"] + change_denominator = params["base_fee_change_denominator"] + call = w3.provider.make_request + tx = {"to": ADDRS["community"], "value": 10, "gasPrice": w3.eth.gas_price} + send_transaction(w3, tx) + method = "eth_feeHistory" + field = "baseFeePerGas" + percentiles = [100] + blocks = [] + histories = [] + for _ in range(3): + b = w3.eth.block_number + blocks.append(b) + histories.append(tuple(call(method, [1, hex(b), percentiles])["result"][field])) + w3_wait_for_new_blocks(w3, 1, 0.1) + blocks.append(w3.eth.block_number) + expected = [] + for b in blocks: + next_base_price = w3.eth.get_block(b).baseFeePerGas + blk = w3.eth.get_block(b - 1) + assert next_base_price == adjust_base_fee( + blk.baseFeePerGas, + blk.gasLimit, + blk.gasUsed, + change_denominator, + elasticity_multiplier, + ) + expected.append(hex(next_base_price)) + assert histories == list(zip(expected, expected[1:])) + + +def test_beyond_head(cluster): + end = hex(0x7fffffffffffffff) + res = cluster.w3.provider.make_request("eth_feeHistory", [4, end, []]) + msg = f"request beyond head block: requested {int(end, 16)}" + assert msg in res["error"]["message"] + + +def test_percentiles(cluster): + w3: Web3 = cluster.w3 + call = w3.provider.make_request + method = "eth_feeHistory" + percentiles = [[-1], [101], [2, 1]] + size = 4 + msg = "invalid reward percentile" + with ThreadPoolExecutor(len(percentiles)) as exec: + tasks = [ + exec.submit(call, method, [size, "latest", p]) for p in percentiles + ] + result = [future.result() for future in as_completed(tasks)] + assert all(msg in res["error"]["message"] for res in result)