From 0efdee34a5a32c38974d8e37dfb16232013b4f29 Mon Sep 17 00:00:00 2001 From: mmsqe Date: Mon, 3 Apr 2023 16:25:09 +0800 Subject: [PATCH] fix(rpc): nextBaseFee for fee history (backport: #1720) (#234) * fix blk number for next base fee * add test * add change doc * cross check next fee * calc base fee based on params elasticity_multiplier & base_fee_change_denominator * concurrent call with maxBlockFetchers * test with get feemarket params * Update CHANGELOG.md --------- Signed-off-by: yihuang Co-authored-by: yihuang --- CHANGELOG.md | 1 + rpc/backend/chain_info.go | 96 ++++++++++++------- rpc/backend/utils.go | 70 ++++++++++++-- .../configs/fee-history.jsonnet | 16 ++++ tests/integration_tests/cosmoscli.py | 4 +- tests/integration_tests/test_fee_history.py | 73 +++++++++++++- 6 files changed, 214 insertions(+), 46 deletions(-) create mode 100644 tests/integration_tests/configs/fee-history.jsonnet diff --git a/CHANGELOG.md b/CHANGELOG.md index a711387e90..adfa25c26a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes +* (rpc) [#1720](https://github.com/evmos/ethermint/pull/1720) Fix next block fee for historical block and calculate base fee by params. * (rpc) [#1722](https://github.com/evmos/ethermint/pull/1722) Align revert response for `eth_estimateGas` and `eth_call` as Ethereum. ### Features diff --git a/rpc/backend/chain_info.go b/rpc/backend/chain_info.go index abee63a461..016477b581 100644 --- a/rpc/backend/chain_info.go +++ b/rpc/backend/chain_info.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" "strconv" + "sync" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common/hexutil" @@ -179,46 +180,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 a63f1dc285..ccaaaf4c6a 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -15,8 +15,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" @@ -24,6 +25,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" ) @@ -102,6 +104,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, @@ -119,11 +162,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 { @@ -135,6 +173,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..b5d15dc24b 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,67 @@ 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:]))