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: lighter gas price oracle for light client #20409

Merged
merged 7 commits into from
Jul 3, 2020
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
11 changes: 8 additions & 3 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,15 +1286,20 @@ func setDataDir(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(LegacyGpoBlocksFlag.Name) {
cfg.Blocks = ctx.GlobalInt(LegacyGpoBlocksFlag.Name)
log.Warn("The flag --gpoblocks is deprecated and will be removed in the future, please use --gpo.blocks")
}
if ctx.GlobalIsSet(GpoBlocksFlag.Name) {
cfg.Blocks = ctx.GlobalInt(GpoBlocksFlag.Name)
}

if ctx.GlobalIsSet(LegacyGpoPercentileFlag.Name) {
cfg.Percentile = ctx.GlobalInt(LegacyGpoPercentileFlag.Name)
log.Warn("The flag --gpopercentile is deprecated and will be removed in the future, please use --gpo.percentile")
Expand Down Expand Up @@ -1503,7 +1508,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
ks = keystores[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)
setMiner(ctx, &cfg.Miner)
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/ethereum/go-ethereum/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.FastSync,
Expand All @@ -59,12 +71,9 @@ var DefaultConfig = Config{
GasPrice: big.NewInt(params.GWei),
Recommit: 3 * time.Second,
},
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
123 changes: 71 additions & 52 deletions eth/gasprice/gasprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
)

const sampleNumber = 3 // Number of transactions sampled in a block

var maxPrice = big.NewInt(500 * params.GWei)

type Config struct {
Expand All @@ -37,21 +38,29 @@ type Config struct {
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,79 +76,79 @@ 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.
// SuggestPrice returns a gasprice so that newly created transaction can
// have a very high chance to be included in the following blocks.
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 {
rjl493456442 marked this conversation as resolved.
Show resolved Hide resolved
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)
}

gpo.cacheLock.Lock()
gpo.lastHead = headHash
gpo.lastPrice = price
Expand All @@ -148,8 +157,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 @@ -159,27 +168,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
Loading