From 6c38b969c827da0a5d449fc95db42c89bf201ed1 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 29 Nov 2019 14:14:50 +0800 Subject: [PATCH 1/7] cmd, eth, les: ligher gas price oracle for light client --- cmd/utils/flags.go | 8 +- eth/backend.go | 2 +- eth/config.go | 21 ++++-- eth/gasprice/gasprice.go | 158 ++++++++++++++++++++++++--------------- les/client.go | 2 +- 5 files changed, 122 insertions(+), 69 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index f928621a13e9..788ef039d30f 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1286,13 +1286,15 @@ 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 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) + } else if light { + cfg.Blocks = eth.DefaultLightGPOConfig.Blocks } if ctx.GlobalIsSet(LegacyGpoPercentileFlag.Name) { @@ -1301,6 +1303,8 @@ func setGPO(ctx *cli.Context, cfg *gasprice.Config) { } if ctx.GlobalIsSet(GpoPercentileFlag.Name) { cfg.Percentile = ctx.GlobalInt(GpoPercentileFlag.Name) + } else if light { + cfg.Percentile = eth.DefaultLightGPOConfig.Percentile } } @@ -1503,7 +1507,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) diff --git a/eth/backend.go b/eth/backend.go index e4f98360dbee..7a43e73eec09 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -224,7 +224,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } - eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams) + eth.APIBackend.gpo = gasprice.NewFullOracle(eth.APIBackend, gpoParams) eth.dialCandidates, err = eth.setupDiscovery(&ctx.Config.P2P) if err != nil { diff --git a/eth/config.go b/eth/config.go index 53acec6c3d4c..6cd0b74d10f9 100644 --- a/eth/config.go +++ b/eth/config.go @@ -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, +} + +// DefaultFullGPOConfig contains default gasprice oracle settings for light client. +var DefaultLightGPOConfig = gasprice.Config{ + Blocks: 2, + Percentile: 50, +} + // DefaultConfig contains default settings for use on the Ethereum main net. var DefaultConfig = Config{ SyncMode: downloader.FastSync, @@ -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 } diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 5d9e75b308b0..7dd08eea2840 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -29,6 +29,11 @@ import ( "github.com/ethereum/go-ethereum/rpc" ) +const ( + fullSampleNumber = 1 // Number of transactions sampled in a block for full node + lightSampleNumber = 3 // Number of transactions sampled in a block for light client +) + var maxPrice = big.NewInt(500 * params.GWei) type Config struct { @@ -40,18 +45,20 @@ type Config struct { // Oracle recommends gas prices based on the content of recent // blocks. Suitable for both light and full clients. type Oracle struct { - backend ethapi.Backend - lastHead common.Hash - lastPrice *big.Int - cacheLock sync.RWMutex - fetchLock sync.Mutex - - checkBlocks, maxEmpty, maxBlocks int - percentile int + backend ethapi.Backend + lastHead common.Hash + defaultPrice *big.Int + lastPrice *big.Int + cacheLock sync.RWMutex + fetchLock sync.Mutex + + checkBlocks, maxInvalid, maxBlocks int + sampleNumber, 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 ethapi.Backend, sampleNumber int, params Config) *Oracle { blocks := params.Blocks if blocks < 1 { blocks = 1 @@ -64,82 +71,101 @@ func NewOracle(backend ethapi.Backend, params Config) *Oracle { percent = 100 } return &Oracle{ - backend: backend, - lastPrice: params.Default, - checkBlocks: blocks, - maxEmpty: blocks / 2, - maxBlocks: blocks * 5, - percentile: percent, + backend: backend, + defaultPrice: params.Default, + lastPrice: params.Default, + checkBlocks: blocks, + maxInvalid: blocks / 2, + maxBlocks: blocks * 5, + sampleNumber: sampleNumber, + percentile: percent, } } -// SuggestPrice returns the recommended gas price. -func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { +// NewFullOracle returns a gasprice oracle for full node which has +// avaiblable recent blocks in local db. FullOracle has higher +// recommendation accuracy. +func NewFullOracle(backend ethapi.Backend, params Config) *Oracle { + return newOracle(backend, fullSampleNumber, params) +} + +// NewLightOracle returns a gasprice oracle for light client which doesn't +// has recent block locally. LightOracle is much cheaper than FullOracle +// however the corresponding recommendation accuracy is lower. +func NewLightOracle(backend ethapi.Backend, params Config) *Oracle { + return newOracle(backend, lightSampleNumber, params) +} + +// getLatest returns a recommended gas price which is suggested last time +// but still suitable now. +func (gpo *Oracle) getLatest(headHash common.Hash) *big.Int { gpo.cacheLock.RLock() lastHead := gpo.lastHead lastPrice := gpo.lastPrice gpo.cacheLock.RUnlock() + if headHash == lastHead { + return lastPrice + } + return nil +} +// SuggesstPrice returns a gasprice so that newly created transaction can +// has very high chance to be included in the following blocks. +func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) headHash := head.Hash() - if headHash == lastHead { - return lastPrice, nil + // Firstly check whether there is available gasprice for recommendation. + if price := gpo.getLatest(headHash); price != nil { + return price, nil } - gpo.fetchLock.Lock() defer gpo.fetchLock.Unlock() - - // try checking the cache again, maybe the last fetch fetched what we need - gpo.cacheLock.RLock() - lastHead = gpo.lastHead - lastPrice = gpo.lastPrice - gpo.cacheLock.RUnlock() - if headHash == lastHead { - return lastPrice, nil + // Try checking the cache again, maybe the last fetch fetched what we need + if price := gpo.getLatest(headHash); price != nil { + return price, 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() + ch = make(chan getBlockPricesResult, gpo.checkBlocks) + txPrices []*big.Int + ) + for sent < gpo.checkBlocks && number > 0 { + go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, gpo.sampleNumber, ch) sent++ exp++ - blockNum-- + number-- } - maxEmpty := gpo.maxEmpty + maxInvalid := gpo.maxInvalid for exp > 0 { res := <-ch if res.err != nil { - return lastPrice, res.err + return gpo.lastPrice, res.err } exp-- - if res.price != nil { - blockPrices = append(blockPrices, res.price) + if res.prices != nil { + txPrices = append(txPrices, res.prices...) continue } - if maxEmpty > 0 { - maxEmpty-- + if maxInvalid > 0 { + maxInvalid-- continue } - if blockNum > 0 && sent < gpo.maxBlocks { - go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch) + if number > 0 && sent < gpo.maxBlocks { + go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, gpo.sampleNumber, ch) sent++ exp++ - blockNum-- + number-- } } - price := lastPrice - if len(blockPrices) > 0 { - sort.Sort(bigIntArray(blockPrices)) - price = blockPrices[(len(blockPrices)-1)*gpo.percentile/100] + price := gpo.lastPrice + 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 @@ -148,8 +174,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 @@ -160,26 +186,40 @@ func (t transactionsByGasPrice) Less(i, j int) bool { return t[i].GasPriceCmp(t[ // 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) { +func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, blockNum uint64, limit int, ch chan getBlockPricesResult) { block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum)) if block == nil { ch <- getBlockPricesResult{nil, err} return } - blockTxs := block.Transactions() + // If the block is empty, it means the lowest gas price is + // enough to let our transaction to be included. + // + // There is a corner case that some miners choose to not include + // any transaction. If so, the recommended gas price is too low. + // However for this case, node can query enough recent blocks. + // In theory, it's very unlikely for all recent miners to intentionally + // choose to not include any transaction. + if len(blockTxs) == 0 { + ch <- getBlockPricesResult{[]*big.Int{gpo.defaultPrice}, nil} + return + } txs := make([]*types.Transaction, len(blockTxs)) copy(txs, blockTxs) sort.Sort(transactionsByGasPrice(txs)) + var result []*big.Int for _, tx := range txs { sender, err := types.Sender(signer, tx) if err == nil && sender != block.Coinbase() { - ch <- getBlockPricesResult{tx.GasPrice(), nil} - return + result = append(result, tx.GasPrice()) + if len(result) >= limit { + break + } } } - ch <- getBlockPricesResult{nil, nil} + ch <- getBlockPricesResult{result, nil} } type bigIntArray []*big.Int diff --git a/les/client.go b/les/client.go index 34a654e22d1e..ab7928a13b3f 100644 --- a/les/client.go +++ b/les/client.go @@ -161,7 +161,7 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } - leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) + leth.ApiBackend.gpo = gasprice.NewLightOracle(leth.ApiBackend, gpoParams) leth.handler = newClientHandler(config.UltraLightServers, config.UltraLightFraction, checkpoint, leth) if leth.handler.ulc != nil { From e654e97c8278fa00d31c459a36478df4eec96dc7 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Sun, 19 Apr 2020 13:44:50 +0800 Subject: [PATCH 2/7] eth, les: address comments --- eth/backend.go | 2 +- eth/config.go | 4 +- eth/gasprice/gasprice.go | 162 ++++++++++++++++----------------------- les/client.go | 2 +- 4 files changed, 72 insertions(+), 98 deletions(-) diff --git a/eth/backend.go b/eth/backend.go index 7a43e73eec09..e4f98360dbee 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -224,7 +224,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } - eth.APIBackend.gpo = gasprice.NewFullOracle(eth.APIBackend, gpoParams) + eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams) eth.dialCandidates, err = eth.setupDiscovery(&ctx.Config.P2P) if err != nil { diff --git a/eth/config.go b/eth/config.go index 6cd0b74d10f9..8547ac177218 100644 --- a/eth/config.go +++ b/eth/config.go @@ -39,10 +39,10 @@ var DefaultFullGPOConfig = gasprice.Config{ Percentile: 60, } -// DefaultFullGPOConfig contains default gasprice oracle settings for light client. +// DefaultLightGPOConfig contains default gasprice oracle settings for light client. var DefaultLightGPOConfig = gasprice.Config{ Blocks: 2, - Percentile: 50, + Percentile: 60, } // DefaultConfig contains default settings for use on the Ethereum main net. diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 7dd08eea2840..f9309c3892cd 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -29,10 +29,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" ) -const ( - fullSampleNumber = 1 // Number of transactions sampled in a block for full node - lightSampleNumber = 3 // Number of transactions sampled in a block for light client -) +const sampleNumber = 3 // Number of transactions sampled in a block var maxPrice = big.NewInt(500 * params.GWei) @@ -45,20 +42,19 @@ type Config struct { // Oracle recommends gas prices based on the content of recent // blocks. Suitable for both light and full clients. type Oracle struct { - backend ethapi.Backend - lastHead common.Hash - defaultPrice *big.Int - lastPrice *big.Int - cacheLock sync.RWMutex - fetchLock sync.Mutex - - checkBlocks, maxInvalid, maxBlocks int - sampleNumber, percentile int + backend ethapi.Backend + lastHead common.Hash + lastPrice *big.Int + cacheLock sync.RWMutex + fetchLock sync.Mutex + + checkBlocks int + percentile int } -// newOracle returns a new gasprice oracle which can recommend suitable +// NewOracle returns a new gasprice oracle which can recommend suitable // gasprice for newly created transaction. -func newOracle(backend ethapi.Backend, sampleNumber int, params Config) *Oracle { +func NewOracle(backend ethapi.Backend, params Config) *Oracle { blocks := params.Blocks if blocks < 1 { blocks = 1 @@ -71,94 +67,76 @@ func newOracle(backend ethapi.Backend, sampleNumber int, params Config) *Oracle percent = 100 } return &Oracle{ - backend: backend, - defaultPrice: params.Default, - lastPrice: params.Default, - checkBlocks: blocks, - maxInvalid: blocks / 2, - maxBlocks: blocks * 5, - sampleNumber: sampleNumber, - percentile: percent, + backend: backend, + lastPrice: params.Default, + checkBlocks: blocks, + percentile: percent, } } -// NewFullOracle returns a gasprice oracle for full node which has -// avaiblable recent blocks in local db. FullOracle has higher -// recommendation accuracy. -func NewFullOracle(backend ethapi.Backend, params Config) *Oracle { - return newOracle(backend, fullSampleNumber, params) -} - -// NewLightOracle returns a gasprice oracle for light client which doesn't -// has recent block locally. LightOracle is much cheaper than FullOracle -// however the corresponding recommendation accuracy is lower. -func NewLightOracle(backend ethapi.Backend, params Config) *Oracle { - return newOracle(backend, lightSampleNumber, params) -} - -// getLatest returns a recommended gas price which is suggested last time -// but still suitable now. -func (gpo *Oracle) getLatest(headHash common.Hash) *big.Int { - gpo.cacheLock.RLock() - lastHead := gpo.lastHead - lastPrice := gpo.lastPrice - gpo.cacheLock.RUnlock() - if headHash == lastHead { - return lastPrice - } - return nil -} - // SuggesstPrice returns a gasprice so that newly created transaction can -// has very high chance to be included in the following blocks. +// have a very high chance to be included in the following blocks. func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) headHash := head.Hash() - // Firstly check whether there is available gasprice for recommendation. - if price := gpo.getLatest(headHash); price != nil { - return price, nil + + // 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 - if price := gpo.getLatest(headHash); price != nil { - return price, nil + gpo.cacheLock.RLock() + lastHead, lastPrice = gpo.lastHead, gpo.lastPrice + gpo.cacheLock.RUnlock() + if headHash == lastHead { + return lastPrice, nil } var ( sent, exp int number = head.Number.Uint64() - ch = make(chan getBlockPricesResult, gpo.checkBlocks) + 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, gpo.sampleNumber, ch) + go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit) sent++ exp++ number-- } - maxInvalid := gpo.maxInvalid for exp > 0 { - res := <-ch + res := <-result if res.err != nil { - return gpo.lastPrice, res.err + close(quit) + return lastPrice, res.err } exp-- - if res.prices != nil { - txPrices = append(txPrices, res.prices...) - continue - } - if maxInvalid > 0 { - maxInvalid-- - continue - } - if number > 0 && sent < gpo.maxBlocks { - go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, gpo.sampleNumber, ch) - sent++ - exp++ - number-- + // 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} + + // 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(txPrices)+1 < gpo.checkBlocks*2 { + go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit) + sent++ + exp++ + number-- + } } + txPrices = append(txPrices, res.prices...) } - price := gpo.lastPrice + price := lastPrice if len(txPrices) > 0 { sort.Sort(bigIntArray(txPrices)) price = txPrices[(len(txPrices)-1)*gpo.percentile/100] @@ -185,41 +163,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, limit int, 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() - // If the block is empty, it means the lowest gas price is - // enough to let our transaction to be included. - // - // There is a corner case that some miners choose to not include - // any transaction. If so, the recommended gas price is too low. - // However for this case, node can query enough recent blocks. - // In theory, it's very unlikely for all recent miners to intentionally - // choose to not include any transaction. - if len(blockTxs) == 0 { - ch <- getBlockPricesResult{[]*big.Int{gpo.defaultPrice}, nil} - return - } txs := make([]*types.Transaction, len(blockTxs)) copy(txs, blockTxs) sort.Sort(transactionsByGasPrice(txs)) - var result []*big.Int + var prices []*big.Int for _, tx := range txs { sender, err := types.Sender(signer, tx) if err == nil && sender != block.Coinbase() { - result = append(result, tx.GasPrice()) - if len(result) >= limit { + prices = append(prices, tx.GasPrice()) + if len(prices) >= limit { break } } } - ch <- getBlockPricesResult{result, nil} + select { + case result <- getBlockPricesResult{prices, nil}: + case <-quit: + } } type bigIntArray []*big.Int diff --git a/les/client.go b/les/client.go index ab7928a13b3f..34a654e22d1e 100644 --- a/les/client.go +++ b/les/client.go @@ -161,7 +161,7 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } - leth.ApiBackend.gpo = gasprice.NewLightOracle(leth.ApiBackend, gpoParams) + leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) leth.handler = newClientHandler(config.UltraLightServers, config.UltraLightFraction, checkpoint, leth) if leth.handler.ulc != nil { From b6be8878b14f6ec457f3d0839ebe3f98609252d2 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Sun, 19 Apr 2020 14:15:55 +0800 Subject: [PATCH 3/7] eth/gasprice: add tests --- eth/gasprice/gasprice.go | 12 +++- eth/gasprice/gasprice_test.go | 116 ++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 eth/gasprice/gasprice_test.go diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index f9309c3892cd..f665f42ece9f 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -24,7 +24,6 @@ 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" ) @@ -39,10 +38,17 @@ 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 @@ -54,7 +60,7 @@ type Oracle struct { // NewOracle returns a new gasprice oracle which can recommend suitable // gasprice for newly created transaction. -func NewOracle(backend ethapi.Backend, params Config) *Oracle { +func NewOracle(backend OracleBackend, params Config) *Oracle { blocks := params.Blocks if blocks < 1 { blocks = 1 diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go new file mode 100644 index 000000000000..caf8bd4cdf6e --- /dev/null +++ b/eth/gasprice/gasprice_test.go @@ -0,0 +1,116 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package gasprice + +import ( + "context" + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" +) + +type testBackend struct { + chain *core.BlockChain +} + +func (b *testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { + if number == rpc.LatestBlockNumber { + return b.chain.CurrentBlock().Header(), nil + } + return b.chain.GetHeaderByNumber(uint64(number)), nil +} + +func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + if number == rpc.LatestBlockNumber { + return b.chain.CurrentBlock(), nil + } + return b.chain.GetBlockByNumber(uint64(number)), nil +} + +func (b *testBackend) ChainConfig() *params.ChainConfig { + return b.chain.Config() +} + +func newTestBackend(t *testing.T) *testBackend { + var ( + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr = crypto.PubkeyToAddress(key.PublicKey) + gspec = &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}}, + } + signer = types.NewEIP155Signer(gspec.Config.ChainID) + ) + engine := ethash.NewFaker() + db := rawdb.NewMemoryDatabase() + genesis, _ := gspec.Commit(db) + + // Generate testing blocks + blocks, _ := core.GenerateChain(params.TestChainConfig, genesis, engine, db, 32, func(i int, b *core.BlockGen) { + b.SetCoinbase(common.Address{1}) + tx, err := types.SignTx(types.NewTransaction(b.TxNonce(addr), common.HexToAddress("deadbeef"), big.NewInt(100), 21000, big.NewInt(int64(i)*params.GWei), nil), signer, key) + if err != nil { + t.Fatalf("failed to create tx: %v", err) + } + b.AddTx(tx) + }) + // Construct testing chain + diskdb := rawdb.NewMemoryDatabase() + gspec.Commit(diskdb) + chain, err := core.NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("Failed to create local chain, %v", err) + } + chain.InsertChain(blocks) + return &testBackend{chain: chain} +} + +func (b *testBackend) CurrentHeader() *types.Header { + return b.chain.CurrentHeader() +} + +func (b *testBackend) GetBlockByNumber(number uint64) *types.Block { + return b.chain.GetBlockByNumber(number) +} + +func TestSuggestPrice(t *testing.T) { + config := Config{ + Blocks: 3, + Percentile: 60, + Default: big.NewInt(params.GWei), + } + backend := newTestBackend(t) + oracle := NewOracle(backend, config) + got, err := oracle.SuggestPrice(context.Background()) + if err != nil { + t.Fatalf("Failed to retrieve recommended gas price: %v", err) + } + expect := big.NewInt(params.GWei * int64(30)) + if got.Cmp(expect) != 0 { + t.Fatalf("Gas price mismatch, want %d, got %d", expect, got) + } +} From 78dd2e209b86f528e905c7f9582f314c20cef1fd Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 22 Apr 2020 15:40:00 +0800 Subject: [PATCH 4/7] eth/gasprice: address comment --- eth/gasprice/gasprice.go | 19 +++++++++---------- eth/gasprice/gasprice_test.go | 4 +++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index f665f42ece9f..51219a5de80a 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -129,16 +129,15 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { // In these cases, use the latest calculated price for samping. if len(res.prices) == 0 { res.prices = []*big.Int{lastPrice} - - // 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(txPrices)+1 < gpo.checkBlocks*2 { - go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit) - sent++ - exp++ - number-- - } + } + // 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++ + number-- } txPrices = append(txPrices, res.prices...) } diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index caf8bd4cdf6e..426d94519ecb 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -72,7 +72,7 @@ func newTestBackend(t *testing.T) *testBackend { // Generate testing blocks blocks, _ := core.GenerateChain(params.TestChainConfig, genesis, engine, db, 32, func(i int, b *core.BlockGen) { b.SetCoinbase(common.Address{1}) - tx, err := types.SignTx(types.NewTransaction(b.TxNonce(addr), common.HexToAddress("deadbeef"), big.NewInt(100), 21000, big.NewInt(int64(i)*params.GWei), nil), signer, key) + tx, err := types.SignTx(types.NewTransaction(b.TxNonce(addr), common.HexToAddress("deadbeef"), big.NewInt(100), 21000, big.NewInt(int64(i+1)*params.GWei), nil), signer, key) if err != nil { t.Fatalf("failed to create tx: %v", err) } @@ -105,6 +105,8 @@ func TestSuggestPrice(t *testing.T) { } backend := newTestBackend(t) oracle := NewOracle(backend, config) + + // The gas price sampled is: 32G, 31G, 30G, 29G, 28G, 27G got, err := oracle.SuggestPrice(context.Background()) if err != nil { t.Fatalf("Failed to retrieve recommended gas price: %v", err) From 8b91e4b03bd9a0c83d4430d9545fa55aa678b21d Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 1 Jul 2020 16:54:28 +0800 Subject: [PATCH 5/7] cmd/utils: fix flag issues --- cmd/utils/flags.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 788ef039d30f..61595e367a01 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1287,24 +1287,25 @@ func setDataDir(ctx *cli.Context, cfg *node.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) - } else if light { - cfg.Blocks = eth.DefaultLightGPOConfig.Blocks } - 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") } if ctx.GlobalIsSet(GpoPercentileFlag.Name) { cfg.Percentile = ctx.GlobalInt(GpoPercentileFlag.Name) - } else if light { - cfg.Percentile = eth.DefaultLightGPOConfig.Percentile } } From 4bbed1290d906df2efe57ddbe7c7d199739a6ed2 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 1 Jul 2020 16:56:56 +0800 Subject: [PATCH 6/7] eth/gasprice: fix typo --- eth/gasprice/gasprice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 51219a5de80a..b9eff3d11a12 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -80,7 +80,7 @@ func NewOracle(backend OracleBackend, params Config) *Oracle { } } -// SuggesstPrice returns a gasprice so that newly created transaction can +// 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) { head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) From 40bc2aac9bca78ac15504d47fa71ce4ab4d8f395 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Thu, 2 Jul 2020 19:36:21 +0800 Subject: [PATCH 7/7] eth/gasprice: fix tests --- eth/gasprice/gasprice_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index 426d94519ecb..89caeeb45bf4 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -81,7 +81,7 @@ func newTestBackend(t *testing.T) *testBackend { // Construct testing chain diskdb := rawdb.NewMemoryDatabase() gspec.Commit(diskdb) - chain, err := core.NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) + chain, err := core.NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil, nil) if err != nil { t.Fatalf("Failed to create local chain, %v", err) }