eth/gasprice: lighter gas price oracle for light client (#20409)

This commit is contained in:
Daniel Liu 2024-05-20 14:00:18 +08:00
parent 0063c14ed3
commit 56591d37e1
4 changed files with 94 additions and 59 deletions

View file

@ -978,7 +978,13 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
} }
} }
func setGPO(ctx *cli.Context, cfg *gasprice.Config) { func setGPO(ctx *cli.Context, cfg *gasprice.Config, light bool) {
// If we are running the light client, apply another group
// settings for gas oracle.
if light {
cfg.Blocks = eth.DefaultLightGPOConfig.Blocks
cfg.Percentile = eth.DefaultLightGPOConfig.Percentile
}
if ctx.GlobalIsSet(GpoBlocksFlag.Name) { if ctx.GlobalIsSet(GpoBlocksFlag.Name) {
cfg.Blocks = ctx.GlobalInt(GpoBlocksFlag.Name) cfg.Blocks = ctx.GlobalInt(GpoBlocksFlag.Name)
} }
@ -1135,7 +1141,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
setEtherbase(ctx, ks, cfg) setEtherbase(ctx, ks, cfg)
setGPO(ctx, &cfg.GPO) setGPO(ctx, &cfg.GPO, ctx.GlobalString(SyncModeFlag.Name) == "light")
setTxPool(ctx, &cfg.TxPool) setTxPool(ctx, &cfg.TxPool)
setEthash(ctx, cfg) setEthash(ctx, cfg)

View file

@ -33,6 +33,18 @@ import (
"github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/params"
) )
// DefaultFullGPOConfig contains default gasprice oracle settings for full node.
var DefaultFullGPOConfig = gasprice.Config{
Blocks: 20,
Percentile: 60,
}
// DefaultLightGPOConfig contains default gasprice oracle settings for light client.
var DefaultLightGPOConfig = gasprice.Config{
Blocks: 2,
Percentile: 60,
}
// DefaultConfig contains default settings for use on the Ethereum main net. // DefaultConfig contains default settings for use on the Ethereum main net.
var DefaultConfig = Config{ var DefaultConfig = Config{
SyncMode: downloader.FullSync, SyncMode: downloader.FullSync,
@ -50,12 +62,9 @@ var DefaultConfig = Config{
TrieTimeout: 5 * time.Minute, TrieTimeout: 5 * time.Minute,
GasPrice: big.NewInt(0.25 * params.Shannon), GasPrice: big.NewInt(0.25 * params.Shannon),
TxPool: core.DefaultTxPoolConfig, TxPool: core.DefaultTxPoolConfig,
RPCGasCap: 25000000, RPCGasCap: 25000000,
GPO: gasprice.Config{ GPO: DefaultFullGPOConfig,
Blocks: 20,
Percentile: 60,
},
RPCTxFeeCap: 1, // 1 ether RPCTxFeeCap: 1, // 1 ether
} }

View file

@ -24,12 +24,13 @@ import (
"github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/internal/ethapi"
"github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/params"
"github.com/XinFinOrg/XDPoSChain/rpc" "github.com/XinFinOrg/XDPoSChain/rpc"
) )
var maxPrice = big.NewInt(500 * params.Shannon) const sampleNumber = 3 // Number of transactions sampled in a block
var maxPrice = big.NewInt(500 * params.GWei)
type Config struct { type Config struct {
Blocks int Blocks int
@ -37,21 +38,29 @@ type Config struct {
Default *big.Int `toml:",omitempty"` Default *big.Int `toml:",omitempty"`
} }
// OracleBackend includes all necessary background APIs for oracle.
type OracleBackend interface {
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
ChainConfig() *params.ChainConfig
}
// Oracle recommends gas prices based on the content of recent // Oracle recommends gas prices based on the content of recent
// blocks. Suitable for both light and full clients. // blocks. Suitable for both light and full clients.
type Oracle struct { type Oracle struct {
backend ethapi.Backend backend OracleBackend
lastHead common.Hash lastHead common.Hash
lastPrice *big.Int lastPrice *big.Int
cacheLock sync.RWMutex cacheLock sync.RWMutex
fetchLock sync.Mutex fetchLock sync.Mutex
checkBlocks, maxEmpty, maxBlocks int checkBlocks int
percentile int percentile int
} }
// NewOracle returns a new oracle. // NewOracle returns a new gasprice oracle which can recommend suitable
func NewOracle(backend ethapi.Backend, params Config) *Oracle { // gasprice for newly created transaction.
func NewOracle(backend OracleBackend, params Config) *Oracle {
blocks := params.Blocks blocks := params.Blocks
if blocks < 1 { if blocks < 1 {
blocks = 1 blocks = 1
@ -67,74 +76,74 @@ func NewOracle(backend ethapi.Backend, params Config) *Oracle {
backend: backend, backend: backend,
lastPrice: params.Default, lastPrice: params.Default,
checkBlocks: blocks, checkBlocks: blocks,
maxEmpty: blocks / 2,
maxBlocks: blocks * 5,
percentile: percent, percentile: percent,
} }
} }
// SuggestPrice returns the recommended gas price. // SuggestPrice returns the recommended gas price.
func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
gpo.cacheLock.RLock()
lastHead := gpo.lastHead
lastPrice := gpo.lastPrice
gpo.cacheLock.RUnlock()
head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash() headHash := head.Hash()
// If the latest gasprice is still available, return it.
gpo.cacheLock.RLock()
lastHead, lastPrice := gpo.lastHead, gpo.lastPrice
gpo.cacheLock.RUnlock()
if headHash == lastHead { if headHash == lastHead {
return lastPrice, nil return lastPrice, nil
} }
gpo.fetchLock.Lock() gpo.fetchLock.Lock()
defer gpo.fetchLock.Unlock() defer gpo.fetchLock.Unlock()
// try checking the cache again, maybe the last fetch fetched what we need // Try checking the cache again, maybe the last fetch fetched what we need
gpo.cacheLock.RLock() gpo.cacheLock.RLock()
lastHead = gpo.lastHead lastHead, lastPrice = gpo.lastHead, gpo.lastPrice
lastPrice = gpo.lastPrice
gpo.cacheLock.RUnlock() gpo.cacheLock.RUnlock()
if headHash == lastHead { if headHash == lastHead {
return lastPrice, nil return lastPrice, nil
} }
var (
blockNum := head.Number.Uint64() sent, exp int
ch := make(chan getBlockPricesResult, gpo.checkBlocks) number = head.Number.Uint64()
sent := 0 result = make(chan getBlockPricesResult, gpo.checkBlocks)
exp := 0 quit = make(chan struct{})
var blockPrices []*big.Int txPrices []*big.Int
for sent < gpo.checkBlocks && blockNum > 0 { )
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch) for sent < gpo.checkBlocks && number > 0 {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit)
sent++ sent++
exp++ exp++
blockNum-- number--
} }
maxEmpty := gpo.maxEmpty
for exp > 0 { for exp > 0 {
res := <-ch res := <-result
if res.err != nil { if res.err != nil {
close(quit)
return lastPrice, res.err return lastPrice, res.err
} }
exp-- exp--
if res.price != nil { // Nothing returned. There are two special cases here:
blockPrices = append(blockPrices, res.price) // - The block is empty
continue // - All the transactions included are sent by the miner itself.
// In these cases, use the latest calculated price for samping.
if len(res.prices) == 0 {
res.prices = []*big.Int{lastPrice}
} }
if maxEmpty > 0 { // Besides, in order to collect enough data for sampling, if nothing
maxEmpty-- // meaningful returned, try to query more blocks. But the maximum
continue // is 2*checkBlocks.
} if len(res.prices) == 1 && len(txPrices)+1+exp < gpo.checkBlocks*2 && number > 0 {
if blockNum > 0 && sent < gpo.maxBlocks { go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit)
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
sent++ sent++
exp++ exp++
blockNum-- number--
} }
txPrices = append(txPrices, res.prices...)
} }
price := lastPrice price := lastPrice
if len(blockPrices) > 0 { if len(txPrices) > 0 {
sort.Sort(bigIntArray(blockPrices)) sort.Sort(bigIntArray(txPrices))
price = blockPrices[(len(blockPrices)-1)*gpo.percentile/100] price = txPrices[(len(txPrices)-1)*gpo.percentile/100]
} }
if price.Cmp(maxPrice) > 0 { if price.Cmp(maxPrice) > 0 {
price = new(big.Int).Set(maxPrice) price = new(big.Int).Set(maxPrice)
@ -154,8 +163,8 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
} }
type getBlockPricesResult struct { type getBlockPricesResult struct {
price *big.Int prices []*big.Int
err error err error
} }
type transactionsByGasPrice []*types.Transaction type transactionsByGasPrice []*types.Transaction
@ -165,27 +174,37 @@ func (t transactionsByGasPrice) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t transactionsByGasPrice) Less(i, j int) bool { return t[i].GasPriceCmp(t[j]) < 0 } func (t transactionsByGasPrice) Less(i, j int) bool { return t[i].GasPriceCmp(t[j]) < 0 }
// getBlockPrices calculates the lowest transaction gas price in a given block // getBlockPrices calculates the lowest transaction gas price in a given block
// and sends it to the result channel. If the block is empty, price is nil. // and sends it to the result channel. If the block is empty or all transactions
func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, blockNum uint64, ch chan getBlockPricesResult) { // 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 (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, blockNum uint64, limit int, result chan getBlockPricesResult, quit chan struct{}) {
block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum)) block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil { if block == nil {
ch <- getBlockPricesResult{nil, err} select {
case result <- getBlockPricesResult{nil, err}:
case <-quit:
}
return return
} }
blockTxs := block.Transactions() blockTxs := block.Transactions()
txs := make([]*types.Transaction, len(blockTxs)) txs := make([]*types.Transaction, len(blockTxs))
copy(txs, blockTxs) copy(txs, blockTxs)
sort.Sort(transactionsByGasPrice(txs)) sort.Sort(transactionsByGasPrice(txs))
var prices []*big.Int
for _, tx := range txs { for _, tx := range txs {
sender, err := types.Sender(signer, tx) sender, err := types.Sender(signer, tx)
if err == nil && sender != block.Coinbase() { if err == nil && sender != block.Coinbase() {
ch <- getBlockPricesResult{tx.GasPrice(), nil} prices = append(prices, tx.GasPrice())
return if len(prices) >= limit {
break
}
} }
} }
ch <- getBlockPricesResult{nil, nil} select {
case result <- getBlockPricesResult{prices, nil}:
case <-quit:
}
} }
type bigIntArray []*big.Int type bigIntArray []*big.Int

View file

@ -25,6 +25,7 @@ const (
Wei = 1 Wei = 1
Ada = 1e3 Ada = 1e3
Babbage = 1e6 Babbage = 1e6
GWei = 1e9
Shannon = 1e9 Shannon = 1e9
Szabo = 1e12 Szabo = 1e12
Finney = 1e15 Finney = 1e15