Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth/gasprice: fix wrong gas price with empty blocks #732

Merged
merged 6 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package common

import (
"bytes"
"encoding/hex"
"fmt"
"math/big"
Expand Down Expand Up @@ -77,23 +78,44 @@ type Vote struct {
Voter Address
}

// BytesToHash sets b to hash.
// If b is larger than len(h), b will be cropped from the left.
func BytesToHash(b []byte) Hash {
var h Hash
h.SetBytes(b)
return h
}

func StringToHash(s string) Hash { return BytesToHash([]byte(s)) }
func BigToHash(b *big.Int) Hash { return BytesToHash(b.Bytes()) }

// BigToHash sets byte representation of b to hash.
// If b is larger than len(h), b will be cropped from the left.
func BigToHash(b *big.Int) Hash { return BytesToHash(b.Bytes()) }

func Uint64ToHash(b uint64) Hash { return BytesToHash(new(big.Int).SetUint64(b).Bytes()) }
func HexToHash(s string) Hash { return BytesToHash(FromHex(s)) }

// HexToHash sets byte representation of s to hash.
// If b is larger than len(h), b will be cropped from the left.
func HexToHash(s string) Hash { return BytesToHash(FromHex(s)) }

// Cmp compares two hashes.
func (h Hash) Cmp(other Hash) int {
return bytes.Compare(h[:], other[:])
}

// IsZero returns if a Hash is empty
func (h Hash) IsZero() bool { return h == Hash{} }

// Get the string representation of the underlying hash
func (h Hash) Str() string { return string(h[:]) }

// Bytes gets the byte representation of the underlying hash.
func (h Hash) Bytes() []byte { return h[:] }

// Big converts a hash to a big integer.
func (h Hash) Big() *big.Int { return new(big.Int).SetBytes(h[:]) }

// Hex converts a hash to a hex string.
func (h Hash) Hex() string { return hexutil.Encode(h[:]) }

// TerminalString implements log.TerminalStringer, formatting a string for console
Expand Down Expand Up @@ -129,7 +151,8 @@ func (h Hash) MarshalText() ([]byte, error) {
return hexutil.Bytes(h[:]).MarshalText()
}

// Sets the hash to the value of b. If b is larger than len(h), 'b' will be cropped (from the left).
// SetBytes sets the hash to the value of b.
// If b is larger than len(h), b will be cropped from the left.
func (h *Hash) SetBytes(b []byte) {
if len(b) > len(h) {
b = b[len(b)-HashLength:]
Expand Down
6 changes: 1 addition & 5 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,7 @@ func New(ctx *node.ServiceContext, config *ethconfig.Config, XDCXServ *XDCx.XDCX
} else {
eth.ApiBackend = &EthApiBackend{eth, nil, nil}
}
gpoParams := config.GPO
if gpoParams.Default == nil {
gpoParams.Default = config.GasPrice
}
eth.ApiBackend.gpo = gasprice.NewOracle(eth.ApiBackend, gpoParams)
eth.ApiBackend.gpo = gasprice.NewOracle(eth.ApiBackend, config.GPO, config.GasPrice)

// Set global ipc endpoint.
eth.blockchain.IPCEndpoint = ctx.GetConfig().IPCEndpoint()
Expand Down
39 changes: 16 additions & 23 deletions eth/gasprice/feehistory.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"fmt"
"math"
"math/big"
"sort"
"slices"
"sync/atomic"

"github.com/XinFinOrg/XDPoSChain/common"
Expand Down Expand Up @@ -56,28 +56,22 @@ type blockFees struct {
err error
}

// processedFees contains the results of a processed block and is also used for caching
type cacheKey struct {
number uint64
percentiles string
}

// processedFees contains the results of a processed block.
type processedFees struct {
reward []*big.Int
baseFee, nextBaseFee *big.Int
gasUsedRatio float64
}

// txGasAndReward is sorted in ascending order based on reward
type (
txGasAndReward struct {
gasUsed uint64
reward *big.Int
}
sortGasAndReward []txGasAndReward
)

func (s sortGasAndReward) Len() int { return len(s) }
func (s sortGasAndReward) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortGasAndReward) Less(i, j int) bool {
return s[i].reward.Cmp(s[j].reward) < 0
type txGasAndReward struct {
gasUsed uint64
reward *big.Int
}

// processBlock takes a blockFees structure with the blockNumber, the header and optionally
Expand Down Expand Up @@ -112,12 +106,14 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
return
}

sorter := make(sortGasAndReward, len(bf.block.Transactions()))
sorter := make([]txGasAndReward, len(bf.block.Transactions()))
for i, tx := range bf.block.Transactions() {
reward, _ := tx.EffectiveGasTip(bf.block.BaseFee())
sorter[i] = txGasAndReward{gasUsed: bf.receipts[i].GasUsed, reward: reward}
}
sort.Stable(sorter)
slices.SortStableFunc(sorter, func(a, b txGasAndReward) int {
return a.reward.Cmp(b.reward)
})

var txIndex int
sumGasUsed := sorter[0].gasUsed
Expand Down Expand Up @@ -268,13 +264,10 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks uint64, unresolvedL
oracle.processBlock(fees, rewardPercentiles)
results <- fees
} else {
cacheKey := struct {
number uint64
percentiles string
}{blockNumber, string(percentileKey)}
cacheKey := cacheKey{number: blockNumber, percentiles: string(percentileKey)}

if p, ok := oracle.historyCache.Get(cacheKey); ok {
fees.results = p.(processedFees)
fees.results = p
results <- fees
} else {
if len(rewardPercentiles) != 0 {
Expand Down
2 changes: 1 addition & 1 deletion eth/gasprice/feehistory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestFeeHistory(t *testing.T) {
MaxBlockHistory: c.maxBlock,
}
backend := newTestBackend(t, big.NewInt(16), c.pending)
oracle := NewOracle(backend, config)
oracle := NewOracle(backend, config, nil)

first, reward, baseFee, ratio, err := oracle.FeeHistory(context.Background(), c.count, c.last, c.percent)

Expand Down
99 changes: 45 additions & 54 deletions eth/gasprice/gasprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ package gasprice
import (
"context"
"math/big"
"sort"
"slices"
"sync"

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/common/lru"
"github.com/XinFinOrg/XDPoSChain/core"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/event"
"github.com/XinFinOrg/XDPoSChain/log"
"github.com/XinFinOrg/XDPoSChain/params"
"github.com/XinFinOrg/XDPoSChain/rpc"
lru "github.com/hashicorp/golang-lru"
)

const sampleNumber = 3 // Number of transactions sampled in a block
Expand All @@ -44,7 +44,6 @@ type Config struct {
Percentile int
MaxHeaderHistory uint64
MaxBlockHistory uint64
Default *big.Int `toml:",omitempty"`
MaxPrice *big.Int `toml:",omitempty"`
IgnorePrice *big.Int `toml:",omitempty"`
}
Expand Down Expand Up @@ -72,12 +71,13 @@ type Oracle struct {

checkBlocks, percentile int
maxHeaderHistory, maxBlockHistory uint64
historyCache *lru.Cache

historyCache *lru.Cache[cacheKey, processedFees]
}

// NewOracle returns a new gasprice oracle which can recommend suitable
// gasprice for newly created transaction.
func NewOracle(backend OracleBackend, params Config) *Oracle {
func NewOracle(backend OracleBackend, params Config, startPrice *big.Int) *Oracle {
blocks := params.Blocks
if blocks < 1 {
blocks = 1
Expand All @@ -87,8 +87,7 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
if percent < 0 {
percent = 0
log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent)
}
if percent > 100 {
} else if percent > 100 {
percent = 100
log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent)
}
Expand All @@ -104,8 +103,21 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {
} else if ignorePrice.Int64() > 0 {
log.Info("Gasprice oracle is ignoring threshold set", "threshold", ignorePrice)
}
maxHeaderHistory := params.MaxHeaderHistory
if maxHeaderHistory < 1 {
maxHeaderHistory = 1
log.Warn("Sanitizing invalid gasprice oracle max header history", "provided", params.MaxHeaderHistory, "updated", maxHeaderHistory)
}
maxBlockHistory := params.MaxBlockHistory
if maxBlockHistory < 1 {
maxBlockHistory = 1
log.Warn("Sanitizing invalid gasprice oracle max block history", "provided", params.MaxBlockHistory, "updated", maxBlockHistory)
}
if startPrice == nil {
startPrice = new(big.Int)
}

cache, _ := lru.New(2048)
cache := lru.NewCache[cacheKey, processedFees](2048)
headEvent := make(chan core.ChainHeadEvent, 1)
backend.SubscribeChainHeadEvent(headEvent)
go func() {
Expand All @@ -120,13 +132,13 @@ func NewOracle(backend OracleBackend, params Config) *Oracle {

return &Oracle{
backend: backend,
lastPrice: params.Default,
lastPrice: startPrice,
maxPrice: maxPrice,
ignorePrice: ignorePrice,
checkBlocks: blocks,
percentile: percent,
maxHeaderHistory: params.MaxHeaderHistory,
maxBlockHistory: params.MaxBlockHistory,
maxHeaderHistory: maxHeaderHistory,
maxBlockHistory: maxBlockHistory,
historyCache: cache,
}
}
Expand Down Expand Up @@ -166,7 +178,7 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
results []*big.Int
)
for sent < oracle.checkBlocks && number > 0 {
go oracle.getBlockValues(ctx, types.MakeSigner(oracle.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, oracle.ignorePrice, result, quit)
go oracle.getBlockValues(ctx, number, sampleNumber, oracle.ignorePrice, result, quit)
sent++
exp++
number--
Expand All @@ -181,15 +193,15 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
// Nothing returned. There are two special cases here:
// - The block is empty
// - All the transactions included are sent by the miner itself.
// In these cases, use the latest calculated price for samping.
// In these cases, use half of the latest calculated price for samping.
if len(res.values) == 0 {
res.values = []*big.Int{lastPrice}
res.values = []*big.Int{new(big.Int).Div(lastPrice, common.Big2)}
}
// Besides, in order to collect enough data for sampling, if nothing
// meaningful returned, try to query more blocks. But the maximum
// is 2*checkBlocks.
if len(res.values) == 1 && len(results)+1+exp < oracle.checkBlocks*2 && number > 0 {
go oracle.getBlockValues(ctx, types.MakeSigner(oracle.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, oracle.ignorePrice, result, quit)
go oracle.getBlockValues(ctx, number, sampleNumber, oracle.ignorePrice, result, quit)
sent++
exp++
number--
Expand All @@ -198,7 +210,7 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
}
price := lastPrice
if len(results) > 0 {
sort.Sort(bigIntArray(results))
slices.SortFunc(results, func(a, b *big.Int) int { return a.Cmp(b) })
price = results[(len(results)-1)*oracle.percentile/100]
}
if price.Cmp(oracle.maxPrice) > 0 {
Expand Down Expand Up @@ -226,35 +238,11 @@ type results struct {
err error
}

type txSorter struct {
txs []*types.Transaction
baseFee *big.Int
}

func newSorter(txs []*types.Transaction, baseFee *big.Int) *txSorter {
return &txSorter{
txs: txs,
baseFee: baseFee,
}
}

func (s *txSorter) Len() int { return len(s.txs) }
func (s *txSorter) Swap(i, j int) {
s.txs[i], s.txs[j] = s.txs[j], s.txs[i]
}
func (s *txSorter) Less(i, j int) bool {
// It's okay to discard the error because a tx would never be
// accepted into a block with an invalid effective tip.
tip1, _ := s.txs[i].EffectiveGasTip(s.baseFee)
tip2, _ := s.txs[j].EffectiveGasTip(s.baseFee)
return tip1.Cmp(tip2) < 0
}

// getBlockPrices calculates the lowest transaction gas price in a given block
// getBlockValues calculates the lowest transaction gas price in a given block
// and sends it to the result channel. If the block is empty or all transactions
// are sent by the miner itself(it doesn't make any sense to include this kind of
// transaction prices for sampling), nil gasprice is returned.
func (oracle *Oracle) getBlockValues(ctx context.Context, signer types.Signer, blockNum uint64, limit int, ignoreUnder *big.Int, result chan results, quit chan struct{}) {
func (oracle *Oracle) getBlockValues(ctx context.Context, blockNum uint64, limit int, ignoreUnder *big.Int, result chan results, quit chan struct{}) {
block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
select {
Expand All @@ -263,15 +251,24 @@ func (oracle *Oracle) getBlockValues(ctx context.Context, signer types.Signer, b
}
return
}
signer := types.MakeSigner(oracle.backend.ChainConfig(), block.Number())

// Sort the transaction by effective tip in ascending sort.
txs := make([]*types.Transaction, len(block.Transactions()))
copy(txs, block.Transactions())
sorter := newSorter(txs, block.BaseFee())
sort.Sort(sorter)
txs := block.Transactions()
sortedTxs := make([]*types.Transaction, len(txs))
copy(sortedTxs, txs)
baseFee := block.BaseFee()
slices.SortFunc(sortedTxs, func(a, b *types.Transaction) int {
// It's okay to discard the error because a tx would never be
// accepted into a block with an invalid effective tip.
tip1, _ := a.EffectiveGasTip(baseFee)
tip2, _ := b.EffectiveGasTip(baseFee)
return tip1.Cmp(tip2)
})

var prices []*big.Int
for _, tx := range sorter.txs {
tip, _ := tx.EffectiveGasTip(block.BaseFee())
for _, tx := range sortedTxs {
tip, _ := tx.EffectiveGasTip(baseFee)
if ignoreUnder != nil && tip.Cmp(ignoreUnder) == -1 {
continue
}
Expand All @@ -288,9 +285,3 @@ func (oracle *Oracle) getBlockValues(ctx context.Context, signer types.Signer, b
case <-quit:
}
}

type bigIntArray []*big.Int

func (s bigIntArray) Len() int { return len(s) }
func (s bigIntArray) Less(i, j int) bool { return s[i].Cmp(s[j]) < 0 }
func (s bigIntArray) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
3 changes: 1 addition & 2 deletions eth/gasprice/gasprice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ func TestSuggestTipCap(t *testing.T) {
config := Config{
Blocks: 3,
Percentile: 60,
Default: big.NewInt(params.GWei),
}
var cases = []struct {
fork *big.Int // Eip1559 fork number
Expand All @@ -188,7 +187,7 @@ func TestSuggestTipCap(t *testing.T) {
}
for _, c := range cases {
backend := newTestBackend(t, c.fork, false)
oracle := NewOracle(backend, config)
oracle := NewOracle(backend, config, big.NewInt(params.GWei))

// The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G
got, err := oracle.SuggestTipCap(context.Background())
Expand Down
Loading