Skip to content

Commit

Permalink
eth/gasprice: lighter gas price oracle for light client (ethereum#20409)
Browse files Browse the repository at this point in the history
  • Loading branch information
gzliudan committed Jun 20, 2024
1 parent 0063c14 commit 56591d3
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 59 deletions.
10 changes: 8 additions & 2 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
cfg.Blocks = ctx.GlobalInt(GpoBlocksFlag.Name)
}
Expand Down Expand Up @@ -1135,7 +1141,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {

ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
setEtherbase(ctx, ks, cfg)
setGPO(ctx, &cfg.GPO)
setGPO(ctx, &cfg.GPO, ctx.GlobalString(SyncModeFlag.Name) == "light")
setTxPool(ctx, &cfg.TxPool)
setEthash(ctx, cfg)

Expand Down
21 changes: 15 additions & 6 deletions eth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ import (
"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.
var DefaultConfig = Config{
SyncMode: downloader.FullSync,
Expand All @@ -50,12 +62,9 @@ var DefaultConfig = Config{
TrieTimeout: 5 * time.Minute,
GasPrice: big.NewInt(0.25 * params.Shannon),

TxPool: core.DefaultTxPoolConfig,
RPCGasCap: 25000000,
GPO: gasprice.Config{
Blocks: 20,
Percentile: 60,
},
TxPool: core.DefaultTxPoolConfig,
RPCGasCap: 25000000,
GPO: DefaultFullGPOConfig,
RPCTxFeeCap: 1, // 1 ether
}

Expand Down
121 changes: 70 additions & 51 deletions eth/gasprice/gasprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,43 @@ import (

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/internal/ethapi"
"github.com/XinFinOrg/XDPoSChain/params"
"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 {
Blocks int
Percentile int
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
// blocks. Suitable for both light and full clients.
type Oracle struct {
backend ethapi.Backend
backend OracleBackend
lastHead common.Hash
lastPrice *big.Int
cacheLock sync.RWMutex
fetchLock sync.Mutex

checkBlocks, maxEmpty, maxBlocks int
percentile int
checkBlocks int
percentile int
}

// NewOracle returns a new oracle.
func NewOracle(backend ethapi.Backend, params Config) *Oracle {
// NewOracle returns a new gasprice oracle which can recommend suitable
// gasprice for newly created transaction.
func NewOracle(backend OracleBackend, params Config) *Oracle {
blocks := params.Blocks
if blocks < 1 {
blocks = 1
Expand All @@ -67,74 +76,74 @@ func NewOracle(backend ethapi.Backend, params Config) *Oracle {
backend: backend,
lastPrice: params.Default,
checkBlocks: blocks,
maxEmpty: blocks / 2,
maxBlocks: blocks * 5,
percentile: percent,
}
}

// SuggestPrice returns the recommended gas price.
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)
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 {
return lastPrice, nil
}

gpo.fetchLock.Lock()
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()
lastHead = gpo.lastHead
lastPrice = gpo.lastPrice
lastHead, lastPrice = gpo.lastHead, gpo.lastPrice
gpo.cacheLock.RUnlock()
if headHash == lastHead {
return lastPrice, nil
}

blockNum := head.Number.Uint64()
ch := make(chan getBlockPricesResult, gpo.checkBlocks)
sent := 0
exp := 0
var blockPrices []*big.Int
for sent < gpo.checkBlocks && blockNum > 0 {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
var (
sent, exp int
number = head.Number.Uint64()
result = make(chan getBlockPricesResult, gpo.checkBlocks)
quit = make(chan struct{})
txPrices []*big.Int
)
for sent < gpo.checkBlocks && number > 0 {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit)
sent++
exp++
blockNum--
number--
}
maxEmpty := gpo.maxEmpty
for exp > 0 {
res := <-ch
res := <-result
if res.err != nil {
close(quit)
return lastPrice, res.err
}
exp--
if res.price != nil {
blockPrices = append(blockPrices, res.price)
continue
}
if maxEmpty > 0 {
maxEmpty--
continue
// 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.
if len(res.prices) == 0 {
res.prices = []*big.Int{lastPrice}
}
if blockNum > 0 && sent < gpo.maxBlocks {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
// 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.prices) == 1 && len(txPrices)+1+exp < gpo.checkBlocks*2 && number > 0 {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit)
sent++
exp++
blockNum--
number--
}
txPrices = append(txPrices, res.prices...)
}
price := lastPrice
if len(blockPrices) > 0 {
sort.Sort(bigIntArray(blockPrices))
price = blockPrices[(len(blockPrices)-1)*gpo.percentile/100]
if len(txPrices) > 0 {
sort.Sort(bigIntArray(txPrices))
price = txPrices[(len(txPrices)-1)*gpo.percentile/100]
}
if price.Cmp(maxPrice) > 0 {
price = new(big.Int).Set(maxPrice)
Expand All @@ -154,8 +163,8 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
}

type getBlockPricesResult struct {
price *big.Int
err error
prices []*big.Int
err error
}

type transactionsByGasPrice []*types.Transaction
Expand All @@ -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 }

// 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.
func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, blockNum uint64, ch chan getBlockPricesResult) {
// 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 (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))
if block == nil {
ch <- getBlockPricesResult{nil, err}
select {
case result <- getBlockPricesResult{nil, err}:
case <-quit:
}
return
}

blockTxs := block.Transactions()
txs := make([]*types.Transaction, len(blockTxs))
copy(txs, blockTxs)
sort.Sort(transactionsByGasPrice(txs))

var prices []*big.Int
for _, tx := range txs {
sender, err := types.Sender(signer, tx)
if err == nil && sender != block.Coinbase() {
ch <- getBlockPricesResult{tx.GasPrice(), nil}
return
prices = append(prices, tx.GasPrice())
if len(prices) >= limit {
break
}
}
}
ch <- getBlockPricesResult{nil, nil}
select {
case result <- getBlockPricesResult{prices, nil}:
case <-quit:
}
}

type bigIntArray []*big.Int
Expand Down
1 change: 1 addition & 0 deletions params/denomination.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
Wei = 1
Ada = 1e3
Babbage = 1e6
GWei = 1e9
Shannon = 1e9
Szabo = 1e12
Finney = 1e15
Expand Down

0 comments on commit 56591d3

Please sign in to comment.