diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 9ffb2c679..eaabc58de 100644 Binary files a/build/openrpc/full.json.gz and b/build/openrpc/full.json.gz differ diff --git a/build/openrpc/gateway.json.gz b/build/openrpc/gateway.json.gz index 20ba312c7..530198113 100644 Binary files a/build/openrpc/gateway.json.gz and b/build/openrpc/gateway.json.gz differ diff --git a/chain/types/ethtypes/eth_transactions.go b/chain/types/ethtypes/eth_transactions.go index 739c7aaec..2eabde6e5 100644 --- a/chain/types/ethtypes/eth_transactions.go +++ b/chain/types/ethtypes/eth_transactions.go @@ -39,11 +39,19 @@ type EthTx struct { MaxFeePerGas EthBigInt `json:"maxFeePerGas"` MaxPriorityFeePerGas EthBigInt `json:"maxPriorityFeePerGas"` AccessList []EthHash `json:"accessList"` - V EthBigInt `json:"yParity"` + V EthBigInt `json:"v"` R EthBigInt `json:"r"` S EthBigInt `json:"s"` } +func (tx *EthTx) Reward(blkBaseFee big.Int) EthBigInt { + availablePriorityFee := big.Sub(big.Int(tx.MaxFeePerGas), blkBaseFee) + if big.Cmp(big.Int(tx.MaxPriorityFeePerGas), availablePriorityFee) <= 0 { + return tx.MaxPriorityFeePerGas + } + return EthBigInt(availablePriorityFee) +} + type EthTxArgs struct { ChainID int `json:"chainId"` Nonce int `json:"nonce"` diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index c53eee6e1..02509170a 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -2643,7 +2643,7 @@ Response: "accessList": [ "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e" ], - "yParity": "0x0", + "v": "0x0", "r": "0x0", "s": "0x0" } @@ -2682,7 +2682,7 @@ Response: "accessList": [ "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e" ], - "yParity": "0x0", + "v": "0x0", "r": "0x0", "s": "0x0" } @@ -2720,7 +2720,7 @@ Response: "accessList": [ "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e" ], - "yParity": "0x0", + "v": "0x0", "r": "0x0", "s": "0x0" } diff --git a/itests/eth_fee_history_test.go b/itests/eth_fee_history_test.go index 9b256c527..33b4c8ae3 100644 --- a/itests/eth_fee_history_test.go +++ b/itests/eth_fee_history_test.go @@ -37,6 +37,7 @@ func TestEthFeeHistory(t *testing.T) { require.Equal(6, len(history.BaseFeePerGas)) require.Equal(5, len(history.GasUsedRatio)) require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock) + require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{"5", "0x10"}), @@ -45,6 +46,7 @@ func TestEthFeeHistory(t *testing.T) { require.Equal(6, len(history.BaseFeePerGas)) require.Equal(5, len(history.GasUsedRatio)) require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock) + require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{"0x10", "0x12"}), @@ -53,6 +55,7 @@ func TestEthFeeHistory(t *testing.T) { require.Equal(17, len(history.BaseFeePerGas)) require.Equal(16, len(history.GasUsedRatio)) require.Equal(ethtypes.EthUint64(18-16+1), history.OldestBlock) + require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "0x10"}), @@ -61,6 +64,7 @@ func TestEthFeeHistory(t *testing.T) { require.Equal(6, len(history.BaseFeePerGas)) require.Equal(5, len(history.GasUsedRatio)) require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock) + require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "10"}), @@ -69,19 +73,28 @@ func TestEthFeeHistory(t *testing.T) { require.Equal(6, len(history.BaseFeePerGas)) require.Equal(5, len(history.GasUsedRatio)) require.Equal(ethtypes.EthUint64(10-5+1), history.OldestBlock) + require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( - json.Marshal([]interface{}{5, "10", &[]float64{0.25, 0.50, 0.75}}), + json.Marshal([]interface{}{5, "10", &[]float64{25, 50, 75}}), ).Assert(require.NoError)) require.NoError(err) require.Equal(6, len(history.BaseFeePerGas)) require.Equal(5, len(history.GasUsedRatio)) require.Equal(ethtypes.EthUint64(10-5+1), history.OldestBlock) require.NotNil(history.Reward) - require.Equal(0, len(*history.Reward)) + require.Equal(5, len(*history.Reward)) + for _, arr := range *history.Reward { + require.Equal(3, len(arr)) + } history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( - json.Marshal([]interface{}{1025, "10", &[]float64{0.25, 0.50, 0.75}}), + json.Marshal([]interface{}{1025, "10", &[]float64{25, 50, 75}}), ).Assert(require.NoError)) require.Error(err) + + history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( + json.Marshal([]interface{}{5, "10", &[]float64{}}), + ).Assert(require.NoError)) + require.NoError(err) } diff --git a/itests/specs/eth_openrpc.json b/itests/specs/eth_openrpc.json index ed13614e9..1947bbd32 100644 --- a/itests/specs/eth_openrpc.json +++ b/itests/specs/eth_openrpc.json @@ -178,7 +178,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -253,8 +253,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -285,7 +285,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -354,8 +354,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -649,7 +649,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -724,8 +724,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -756,7 +756,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -825,8 +825,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -3009,7 +3009,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -3084,8 +3084,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -3116,7 +3116,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -3185,8 +3185,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -3359,7 +3359,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -3434,8 +3434,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -3466,7 +3466,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -3535,8 +3535,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -3726,7 +3726,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -3801,8 +3801,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." @@ -3833,7 +3833,7 @@ "s", "type", "value", - "yParity" + "v" ], "properties": { "type": { @@ -3902,8 +3902,8 @@ "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "Chain ID that this transaction is valid on." }, - "yParity": { - "title": "yParity", + "v": { + "title": "v", "type": "string", "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$", "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature." diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index eaa093e45..428f56d80 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strconv" "sync" "time" @@ -602,6 +603,18 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth if params.BlkCount > 1024 { return ethtypes.EthFeeHistory{}, fmt.Errorf("block count should be smaller than 1024") } + rewardPercentiles := make([]float64, 0) + if params.RewardPercentiles != nil { + rewardPercentiles = append(rewardPercentiles, *params.RewardPercentiles...) + } + for i, rp := range rewardPercentiles { + if rp < 0 || rp > 100 { + return ethtypes.EthFeeHistory{}, fmt.Errorf("invalid reward percentile: %f should be between 0 and 100", rp) + } + if i > 0 && rp < rewardPercentiles[i-1] { + return ethtypes.EthFeeHistory{}, fmt.Errorf("invalid reward percentile: %f should be larger than %f", rp, rewardPercentiles[i-1]) + } + } ts, err := a.parseBlkParam(ctx, params.NewestBlkNum) if err != nil { @@ -620,18 +633,40 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth // we can do is duplicate the last value. baseFeeArray := []ethtypes.EthBigInt{ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee)} gasUsedRatioArray := []float64{} + rewardsArray := make([][]ethtypes.EthBigInt, 0) for ts.Height() >= abi.ChainEpoch(oldestBlkHeight) { // Unfortunately we need to rebuild the full message view so we can // totalize gas used in the tipset. - block, err := newEthBlockFromFilecoinTipSet(ctx, ts, false, a.Chain, a.StateAPI) + msgs, err := a.Chain.MessagesForTipset(ctx, ts) if err != nil { - return ethtypes.EthFeeHistory{}, fmt.Errorf("cannot create eth block: %v", err) + return ethtypes.EthFeeHistory{}, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) } - // both arrays should be reversed at the end + txGasRewards := gasRewardSorter{} + for txIdx, msg := range msgs { + msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, msg.Cid(), api.LookbackNoLimit, false) + if err != nil || msgLookup == nil { + return ethtypes.EthFeeHistory{}, nil + } + + tx, err := newEthTxFromMessageLookup(ctx, msgLookup, txIdx, a.Chain, a.StateAPI) + if err != nil { + return ethtypes.EthFeeHistory{}, nil + } + + txGasRewards = append(txGasRewards, gasRewardTuple{ + reward: tx.Reward(ts.Blocks()[0].ParentBaseFee), + gas: uint64(msgLookup.Receipt.GasUsed), + }) + } + + rewards, totalGasUsed := calculateRewardsAndGasUsed(rewardPercentiles, txGasRewards) + + // arrays should be reversed at the end baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee)) - gasUsedRatioArray = append(gasUsedRatioArray, float64(block.GasUsed)/float64(build.BlockGasLimit)) + gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(build.BlockGasLimit)) + rewardsArray = append(rewardsArray, rewards) parentTsKey := ts.Parents() ts, err = a.Chain.LoadTipSet(ctx, parentTsKey) @@ -647,6 +682,9 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth for i, j := 0, len(gasUsedRatioArray)-1; i < j; i, j = i+1, j-1 { gasUsedRatioArray[i], gasUsedRatioArray[j] = gasUsedRatioArray[j], gasUsedRatioArray[i] } + for i, j := 0, len(rewardsArray)-1; i < j; i, j = i+1, j-1 { + rewardsArray[i], rewardsArray[j] = rewardsArray[j], rewardsArray[i] + } ret := ethtypes.EthFeeHistory{ OldestBlock: ethtypes.EthUint64(oldestBlkHeight), @@ -654,13 +692,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth GasUsedRatio: gasUsedRatioArray, } if params.RewardPercentiles != nil { - // TODO: Populate reward percentiles - // https://github.com/filecoin-project/lotus/issues/10236 - // We need to calculate the requested percentiles of effective gas premium - // based on the newest block (I presume it's the newest, we need to dig in - // as it's underspecified). Effective means we're clamped at the gas_fee_cap - base_fee. - reward := make([][]ethtypes.EthBigInt, 0) - ret.Reward = &reward + ret.Reward = &rewardsArray } return ret, nil } @@ -2135,3 +2167,50 @@ func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) { } return keys, nil } + +func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, uint64) { + var totalGasUsed uint64 + for _, tx := range txGasRewards { + totalGasUsed += tx.gas + } + + rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles)) + for i := range rewards { + rewards[i] = ethtypes.EthBigIntZero + } + + if len(txGasRewards) == 0 { + return rewards, totalGasUsed + } + + sort.Stable(txGasRewards) + + var idx int + var sum uint64 + for i, percentile := range rewardPercentiles { + threshold := uint64(float64(totalGasUsed) * percentile / 100) + for sum < threshold && idx < len(txGasRewards)-1 { + sum += txGasRewards[idx].gas + idx++ + } + rewards[i] = txGasRewards[idx].reward + } + + return rewards, totalGasUsed +} + +type gasRewardTuple struct { + gas uint64 + reward ethtypes.EthBigInt +} + +// sorted in ascending order +type gasRewardSorter []gasRewardTuple + +func (g gasRewardSorter) Len() int { return len(g) } +func (g gasRewardSorter) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} +func (g gasRewardSorter) Less(i, j int) bool { + return g[i].reward.Int.Cmp(g[j].reward.Int) == -1 +} diff --git a/node/impl/full/eth_test.go b/node/impl/full/eth_test.go index 027becf34..67a8b0500 100644 --- a/node/impl/full/eth_test.go +++ b/node/impl/full/eth_test.go @@ -6,6 +6,8 @@ import ( "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" + "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" ) @@ -100,3 +102,65 @@ func TestEthLogFromEvent(t *testing.T) { require.Len(t, topics, 1) require.Equal(t, topics[0], ethtypes.EthHash{}) } + +func TestReward(t *testing.T) { + baseFee := big.NewInt(100) + testcases := []struct { + maxFeePerGas, maxPriorityFeePerGas big.Int + answer big.Int + }{ + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(200)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(300), answer: big.NewInt(300)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(500), answer: big.NewInt(500)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(600), answer: big.NewInt(500)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(1000), answer: big.NewInt(500)}, + {maxFeePerGas: big.NewInt(50), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(-50)}, + } + for _, tc := range testcases { + tx := ethtypes.EthTx{ + MaxFeePerGas: ethtypes.EthBigInt(tc.maxFeePerGas), + MaxPriorityFeePerGas: ethtypes.EthBigInt(tc.maxPriorityFeePerGas), + } + reward := tx.Reward(baseFee) + require.Equal(t, 0, reward.Int.Cmp(tc.answer.Int), reward, tc.answer) + } +} + +func TestRewardPercentiles(t *testing.T) { + testcases := []struct { + percentiles []float64 + txGasRewards gasRewardSorter + answer []int64 + }{ + { + percentiles: []float64{25, 50, 75}, + txGasRewards: []gasRewardTuple{}, + answer: []int64{0, 0, 0}, + }, + { + percentiles: []float64{25, 50, 75, 100}, + txGasRewards: []gasRewardTuple{ + {gas: uint64(0), reward: ethtypes.EthBigInt(big.NewInt(300))}, + {gas: uint64(100), reward: ethtypes.EthBigInt(big.NewInt(200))}, + {gas: uint64(350), reward: ethtypes.EthBigInt(big.NewInt(100))}, + {gas: uint64(500), reward: ethtypes.EthBigInt(big.NewInt(600))}, + {gas: uint64(300), reward: ethtypes.EthBigInt(big.NewInt(700))}, + }, + answer: []int64{200, 700, 700, 700}, + }, + } + for _, tc := range testcases { + rewards, totalGasUsed := calculateRewardsAndGasUsed(tc.percentiles, tc.txGasRewards) + gasUsed := uint64(0) + for _, tx := range tc.txGasRewards { + gasUsed += tx.gas + } + ans := []ethtypes.EthBigInt{} + for _, bi := range tc.answer { + ans = append(ans, ethtypes.EthBigInt(big.NewInt(bi))) + } + require.Equal(t, totalGasUsed, gasUsed) + require.Equal(t, len(ans), len(tc.percentiles)) + require.Equal(t, ans, rewards) + } +}