fix: EthAPI: use StateCompute for feeHistory; apply minimum gas premium (#10413)
This commit is contained in:
parent
1da2d59066
commit
0e58b3fbbc
@ -3,18 +3,42 @@ package itests
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/filecoin-project/go-jsonrpc"
|
"github.com/filecoin-project/go-jsonrpc"
|
||||||
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/lotus/chain/types"
|
||||||
"github.com/filecoin-project/lotus/chain/types/ethtypes"
|
"github.com/filecoin-project/lotus/chain/types/ethtypes"
|
||||||
"github.com/filecoin-project/lotus/itests/kit"
|
"github.com/filecoin-project/lotus/itests/kit"
|
||||||
"github.com/filecoin-project/lotus/lib/result"
|
"github.com/filecoin-project/lotus/lib/result"
|
||||||
|
"github.com/filecoin-project/lotus/node/impl/full"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// calculateExpectations calculates the expected number of items to be included in the response
|
||||||
|
// of eth_feeHistory. It takes care of null rounds by finding the closet tipset with height
|
||||||
|
// smaller than startHeight, and then looks back at requestAmount of items. It also considers
|
||||||
|
// scenarios where there are not enough items to look back.
|
||||||
|
func calculateExpectations(tsHeights []int, requestAmount, startHeight int) (count, oldestHeight int) {
|
||||||
|
latestIdx := sort.SearchInts(tsHeights, startHeight)
|
||||||
|
// SearchInts returns the index of the number that's larger than the target if the target
|
||||||
|
// doesn't exist. However, we're looking for the closet number that's smaller that the target
|
||||||
|
for tsHeights[latestIdx] > startHeight {
|
||||||
|
latestIdx--
|
||||||
|
}
|
||||||
|
cnt := requestAmount
|
||||||
|
oldestIdx := latestIdx - requestAmount + 1
|
||||||
|
if oldestIdx < 0 {
|
||||||
|
cnt = latestIdx + 1
|
||||||
|
oldestIdx = 0
|
||||||
|
}
|
||||||
|
return cnt, tsHeights[oldestIdx]
|
||||||
|
}
|
||||||
|
|
||||||
func TestEthFeeHistory(t *testing.T) {
|
func TestEthFeeHistory(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
@ -22,70 +46,136 @@ func TestEthFeeHistory(t *testing.T) {
|
|||||||
|
|
||||||
blockTime := 100 * time.Millisecond
|
blockTime := 100 * time.Millisecond
|
||||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
||||||
ens.InterconnectAll().BeginMining(blockTime)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Wait for the network to create 20 blocks
|
heads, err := client.ChainNotify(ctx)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Save the full view of the tipsets to calculate the answer when there are null rounds
|
||||||
|
tsHeights := []int{1}
|
||||||
|
go func() {
|
||||||
|
for chg := range heads {
|
||||||
|
for _, c := range chg {
|
||||||
|
tsHeights = append(tsHeights, int(c.Val.Height()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
miner := ens.InterconnectAll().BeginMining(blockTime)
|
||||||
|
|
||||||
|
client.WaitTillChain(ctx, kit.HeightAtLeast(7))
|
||||||
|
miner[0].InjectNulls(abi.ChainEpoch(5))
|
||||||
|
|
||||||
|
// Wait for the network to create at least 20 tipsets
|
||||||
client.WaitTillChain(ctx, kit.HeightAtLeast(20))
|
client.WaitTillChain(ctx, kit.HeightAtLeast(20))
|
||||||
|
for _, m := range miner {
|
||||||
|
m.Pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := client.ChainNotify(ctx)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Wait for 5 seconds of inactivity
|
||||||
|
func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
continue
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sort.Ints(tsHeights)
|
||||||
|
|
||||||
|
// because of the deferred execution, the last tipset is not executed yet,
|
||||||
|
// and the one before the last one is the last executed tipset,
|
||||||
|
// which corresponds to the "latest" tag in EthGetBlockByNumber
|
||||||
|
latestBlk := ethtypes.EthUint64(tsHeights[len(tsHeights)-2])
|
||||||
|
blk, err := client.EthGetBlockByNumber(ctx, "latest", false)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(blk.Number, latestBlk)
|
||||||
|
|
||||||
|
assertHistory := func(history *ethtypes.EthFeeHistory, requestAmount, startHeight int) {
|
||||||
|
amount, oldest := calculateExpectations(tsHeights, requestAmount, startHeight)
|
||||||
|
require.Equal(amount+1, len(history.BaseFeePerGas))
|
||||||
|
require.Equal(amount, len(history.GasUsedRatio))
|
||||||
|
require.Equal(ethtypes.EthUint64(oldest), history.OldestBlock)
|
||||||
|
}
|
||||||
|
|
||||||
history, err := client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err := client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{5, "0x10"}),
|
json.Marshal([]interface{}{5, "0x10"}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(6, len(history.BaseFeePerGas))
|
assertHistory(&history, 5, 16)
|
||||||
require.Equal(5, len(history.GasUsedRatio))
|
|
||||||
require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock)
|
|
||||||
require.Nil(history.Reward)
|
require.Nil(history.Reward)
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{"5", "0x10"}),
|
json.Marshal([]interface{}{"5", "0x10"}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(6, len(history.BaseFeePerGas))
|
assertHistory(&history, 5, 16)
|
||||||
require.Equal(5, len(history.GasUsedRatio))
|
require.Nil(history.Reward)
|
||||||
require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock)
|
|
||||||
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
|
json.Marshal([]interface{}{5, "latest"}),
|
||||||
|
).Assert(require.NoError))
|
||||||
|
require.NoError(err)
|
||||||
|
assertHistory(&history, 5, int(latestBlk))
|
||||||
require.Nil(history.Reward)
|
require.Nil(history.Reward)
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{"0x10", "0x12"}),
|
json.Marshal([]interface{}{"0x10", "0x12"}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(17, len(history.BaseFeePerGas))
|
assertHistory(&history, 16, 18)
|
||||||
require.Equal(16, len(history.GasUsedRatio))
|
|
||||||
require.Equal(ethtypes.EthUint64(18-16+1), history.OldestBlock)
|
|
||||||
require.Nil(history.Reward)
|
require.Nil(history.Reward)
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{5, "0x10"}),
|
json.Marshal([]interface{}{5, "0x10"}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(6, len(history.BaseFeePerGas))
|
assertHistory(&history, 5, 16)
|
||||||
require.Equal(5, len(history.GasUsedRatio))
|
|
||||||
require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock)
|
|
||||||
require.Nil(history.Reward)
|
require.Nil(history.Reward)
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{5, "10"}),
|
json.Marshal([]interface{}{5, "10"}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(6, len(history.BaseFeePerGas))
|
assertHistory(&history, 5, 10)
|
||||||
require.Equal(5, len(history.GasUsedRatio))
|
require.Nil(history.Reward)
|
||||||
require.Equal(ethtypes.EthUint64(10-5+1), history.OldestBlock)
|
|
||||||
|
// test when the requested number of blocks is longer than chain length
|
||||||
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
|
json.Marshal([]interface{}{"0x30", "latest"}),
|
||||||
|
).Assert(require.NoError))
|
||||||
|
require.NoError(err)
|
||||||
|
assertHistory(&history, 48, int(latestBlk))
|
||||||
|
require.Nil(history.Reward)
|
||||||
|
|
||||||
|
// test when the requested number of blocks is longer than chain length
|
||||||
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
|
json.Marshal([]interface{}{"0x30", "10"}),
|
||||||
|
).Assert(require.NoError))
|
||||||
|
require.NoError(err)
|
||||||
|
assertHistory(&history, 48, 10)
|
||||||
require.Nil(history.Reward)
|
require.Nil(history.Reward)
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{5, "10", &[]float64{25, 50, 75}}),
|
json.Marshal([]interface{}{5, "10", &[]float64{25, 50, 75}}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Equal(6, len(history.BaseFeePerGas))
|
assertHistory(&history, 5, 10)
|
||||||
require.Equal(5, len(history.GasUsedRatio))
|
|
||||||
require.Equal(ethtypes.EthUint64(10-5+1), history.OldestBlock)
|
|
||||||
require.NotNil(history.Reward)
|
require.NotNil(history.Reward)
|
||||||
require.Equal(5, len(*history.Reward))
|
require.Equal(5, len(*history.Reward))
|
||||||
for _, arr := range *history.Reward {
|
for _, arr := range *history.Reward {
|
||||||
require.Equal(3, len(arr))
|
require.Equal(3, len(arr))
|
||||||
|
for _, item := range arr {
|
||||||
|
require.Equal(ethtypes.EthBigInt(types.NewInt(full.MinGasPremium)), item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
@ -93,6 +183,11 @@ func TestEthFeeHistory(t *testing.T) {
|
|||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
|
|
||||||
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
|
json.Marshal([]interface{}{5, "10", &[]float64{75, 50}}),
|
||||||
|
).Assert(require.NoError))
|
||||||
|
require.Error(err)
|
||||||
|
|
||||||
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams](
|
||||||
json.Marshal([]interface{}{5, "10", &[]float64{}}),
|
json.Marshal([]interface{}{5, "10", &[]float64{}}),
|
||||||
).Assert(require.NoError))
|
).Assert(require.NoError))
|
||||||
|
@ -254,10 +254,7 @@ func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string, strict b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse block number: %v", err)
|
return nil, fmt.Errorf("cannot parse block number: %v", err)
|
||||||
}
|
}
|
||||||
if abi.ChainEpoch(num) > head.Height()-1 {
|
ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, true)
|
||||||
return nil, fmt.Errorf("requested a future epoch (beyond 'latest')")
|
|
||||||
}
|
|
||||||
ts, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(num), head.Key())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot get tipset at height: %v", num)
|
return nil, fmt.Errorf("cannot get tipset at height: %v", num)
|
||||||
}
|
}
|
||||||
@ -689,11 +686,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth
|
|||||||
return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err)
|
return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deal with the case that the chain is shorter than the number of requested blocks.
|
|
||||||
oldestBlkHeight := uint64(1)
|
oldestBlkHeight := uint64(1)
|
||||||
if abi.ChainEpoch(params.BlkCount) <= ts.Height() {
|
|
||||||
oldestBlkHeight = uint64(ts.Height()) - uint64(params.BlkCount) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: baseFeePerGas should include the next block after the newest of the returned range,
|
// NOTE: baseFeePerGas should include the next block after the newest of the returned range,
|
||||||
// because the next base fee can be inferred from the messages in the newest block.
|
// because the next base fee can be inferred from the messages in the newest block.
|
||||||
@ -703,29 +696,32 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth
|
|||||||
gasUsedRatioArray := []float64{}
|
gasUsedRatioArray := []float64{}
|
||||||
rewardsArray := make([][]ethtypes.EthBigInt, 0)
|
rewardsArray := make([][]ethtypes.EthBigInt, 0)
|
||||||
|
|
||||||
for ts.Height() >= abi.ChainEpoch(oldestBlkHeight) {
|
blocksIncluded := 0
|
||||||
// Unfortunately we need to rebuild the full message view so we can
|
for blocksIncluded < int(params.BlkCount) && ts.Height() > 0 {
|
||||||
// totalize gas used in the tipset.
|
compOutput, err := a.StateCompute(ctx, ts.Height(), nil, ts.Key())
|
||||||
msgs, err := a.Chain.MessagesForTipset(ctx, ts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ethtypes.EthFeeHistory{}, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err)
|
return ethtypes.EthFeeHistory{}, xerrors.Errorf("cannot lookup the status of tipset: %v: %w", ts, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
txGasRewards := gasRewardSorter{}
|
txGasRewards := gasRewardSorter{}
|
||||||
for txIdx, msg := range msgs {
|
for _, msg := range compOutput.Trace {
|
||||||
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, msg.Cid(), api.LookbackNoLimit, false)
|
if msg.Msg.From == builtintypes.SystemActorAddr {
|
||||||
if err != nil || msgLookup == nil {
|
continue
|
||||||
return ethtypes.EthFeeHistory{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := newEthTxFromMessageLookup(ctx, msgLookup, txIdx, a.Chain, a.StateAPI)
|
smsgCid, err := getSignedMessage(ctx, a.Chain, msg.MsgCid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ethtypes.EthFeeHistory{}, nil
|
return ethtypes.EthFeeHistory{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.MsgCid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := newEthTxFromSignedMessage(ctx, smsgCid, a.StateAPI)
|
||||||
|
if err != nil {
|
||||||
|
return ethtypes.EthFeeHistory{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
txGasRewards = append(txGasRewards, gasRewardTuple{
|
txGasRewards = append(txGasRewards, gasRewardTuple{
|
||||||
reward: tx.Reward(ts.Blocks()[0].ParentBaseFee),
|
reward: tx.Reward(ts.Blocks()[0].ParentBaseFee),
|
||||||
gas: uint64(msgLookup.Receipt.GasUsed),
|
gas: uint64(msg.MsgRct.GasUsed),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -735,6 +731,8 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth
|
|||||||
baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee))
|
baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee))
|
||||||
gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(build.BlockGasLimit))
|
gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(build.BlockGasLimit))
|
||||||
rewardsArray = append(rewardsArray, rewards)
|
rewardsArray = append(rewardsArray, rewards)
|
||||||
|
oldestBlkHeight = uint64(ts.Height())
|
||||||
|
blocksIncluded++
|
||||||
|
|
||||||
parentTsKey := ts.Parents()
|
parentTsKey := ts.Parents()
|
||||||
ts, err = a.Chain.LoadTipSet(ctx, parentTsKey)
|
ts, err = a.Chain.LoadTipSet(ctx, parentTsKey)
|
||||||
@ -2351,7 +2349,7 @@ func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRew
|
|||||||
|
|
||||||
rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles))
|
rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles))
|
||||||
for i := range rewards {
|
for i := range rewards {
|
||||||
rewards[i] = ethtypes.EthBigIntZero
|
rewards[i] = ethtypes.EthBigInt(types.NewInt(MinGasPremium))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(txGasRewards) == 0 {
|
if len(txGasRewards) == 0 {
|
||||||
|
@ -135,7 +135,7 @@ func TestRewardPercentiles(t *testing.T) {
|
|||||||
{
|
{
|
||||||
percentiles: []float64{25, 50, 75},
|
percentiles: []float64{25, 50, 75},
|
||||||
txGasRewards: []gasRewardTuple{},
|
txGasRewards: []gasRewardTuple{},
|
||||||
answer: []int64{0, 0, 0},
|
answer: []int64{MinGasPremium, MinGasPremium, MinGasPremium},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
percentiles: []float64{25, 50, 75, 100},
|
percentiles: []float64{25, 50, 75, 100},
|
||||||
|
Loading…
Reference in New Issue
Block a user