cmd, les, eth, eth/gasprice: using new gas price oracle (#13853)

* cmd, les, eth, eth/gasprice: using new gas price oracle

* eth/gasprice: renamed source file

* eth/gasprice: added security checks for gpo params

* eth/gasprice: fixed naming issues

* eth/gasprice: max limit, maxEmpty
This commit is contained in:
Felföldi Zsolt 2017-04-06 16:20:42 +02:00 committed by Péter Szilágyi
parent 0ec1104ba9
commit 9aca9e6deb
10 changed files with 176 additions and 428 deletions

View File

@ -147,12 +147,8 @@ func init() {
utils.FakePoWFlag,
utils.NoCompactionFlag,
utils.SolcPathFlag,
utils.GpoMinGasPriceFlag,
utils.GpoMaxGasPriceFlag,
utils.GpoFullBlockRatioFlag,
utils.GpobaseStepDownFlag,
utils.GpobaseStepUpFlag,
utils.GpobaseCorrectionFactorFlag,
utils.GpoBlocksFlag,
utils.GpoPercentileFlag,
utils.ExtraDataFlag,
}
app.Flags = append(app.Flags, debug.Flags...)

View File

@ -151,12 +151,8 @@ var AppHelpFlagGroups = []flagGroup{
{
Name: "GAS PRICE ORACLE",
Flags: []cli.Flag{
utils.GpoMinGasPriceFlag,
utils.GpoMaxGasPriceFlag,
utils.GpoFullBlockRatioFlag,
utils.GpobaseStepDownFlag,
utils.GpobaseStepUpFlag,
utils.GpobaseCorrectionFactorFlag,
utils.GpoBlocksFlag,
utils.GpoPercentileFlag,
},
},
{

View File

@ -409,35 +409,15 @@ var (
}
// Gas price oracle settings
GpoMinGasPriceFlag = BigFlag{
Name: "gpomin",
Usage: "Minimum suggested gas price",
Value: big.NewInt(20 * params.Shannon),
}
GpoMaxGasPriceFlag = BigFlag{
Name: "gpomax",
Usage: "Maximum suggested gas price",
Value: big.NewInt(500 * params.Shannon),
}
GpoFullBlockRatioFlag = cli.IntFlag{
Name: "gpofull",
Usage: "Full block threshold for gas price calculation (%)",
Value: 80,
}
GpobaseStepDownFlag = cli.IntFlag{
Name: "gpobasedown",
Usage: "Suggested gas price base step down ratio (1/1000)",
GpoBlocksFlag = cli.IntFlag{
Name: "gpoblocks",
Usage: "Number of recent blocks to check for gas prices",
Value: 10,
}
GpobaseStepUpFlag = cli.IntFlag{
Name: "gpobaseup",
Usage: "Suggested gas price base step up ratio (1/1000)",
Value: 100,
}
GpobaseCorrectionFactorFlag = cli.IntFlag{
Name: "gpobasecf",
Usage: "Suggested gas price base correction factor (%)",
Value: 110,
GpoPercentileFlag = cli.IntFlag{
Name: "gpopercentile",
Usage: "Suggested gas price is the given percentile of a set of recent transaction gas prices",
Value: 50,
}
)
@ -798,12 +778,8 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) {
ExtraData: MakeMinerExtra(extra, ctx),
DocRoot: ctx.GlobalString(DocRootFlag.Name),
GasPrice: GlobalBig(ctx, GasPriceFlag.Name),
GpoMinGasPrice: GlobalBig(ctx, GpoMinGasPriceFlag.Name),
GpoMaxGasPrice: GlobalBig(ctx, GpoMaxGasPriceFlag.Name),
GpoFullBlockRatio: ctx.GlobalInt(GpoFullBlockRatioFlag.Name),
GpobaseStepDown: ctx.GlobalInt(GpobaseStepDownFlag.Name),
GpobaseStepUp: ctx.GlobalInt(GpobaseStepUpFlag.Name),
GpobaseCorrectionFactor: ctx.GlobalInt(GpobaseCorrectionFactorFlag.Name),
GpoBlocks: ctx.GlobalInt(GpoBlocksFlag.Name),
GpoPercentile: ctx.GlobalInt(GpoPercentileFlag.Name),
SolcPath: ctx.GlobalString(SolcPathFlag.Name),
EthashCacheDir: MakeEthashCacheDir(ctx),
EthashCachesInMem: ctx.GlobalInt(EthashCachesInMemoryFlag.Name),

View File

@ -39,7 +39,7 @@ import (
// EthApiBackend implements ethapi.Backend for full nodes
type EthApiBackend struct {
eth *Ethereum
gpo *gasprice.GasPriceOracle
gpo *gasprice.Oracle
}
func (b *EthApiBackend) ChainConfig() *params.ChainConfig {
@ -186,7 +186,7 @@ func (b *EthApiBackend) ProtocolVersion() int {
}
func (b *EthApiBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPrice(), nil
return b.gpo.SuggestPrice(ctx)
}
func (b *EthApiBackend) ChainDb() ethdb.Database {

View File

@ -84,12 +84,8 @@ type Config struct {
MinerThreads int
SolcPath string
GpoMinGasPrice *big.Int
GpoMaxGasPrice *big.Int
GpoFullBlockRatio int
GpobaseStepDown int
GpobaseStepUp int
GpobaseCorrectionFactor int
GpoBlocks int
GpoPercentile int
EnablePreimageRecording bool
}
@ -211,16 +207,13 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
eth.miner.SetGasPrice(config.GasPrice)
eth.miner.SetExtra(config.ExtraData)
gpoParams := &gasprice.GpoParams{
GpoMinGasPrice: config.GpoMinGasPrice,
GpoMaxGasPrice: config.GpoMaxGasPrice,
GpoFullBlockRatio: config.GpoFullBlockRatio,
GpobaseStepDown: config.GpobaseStepDown,
GpobaseStepUp: config.GpobaseStepUp,
GpobaseCorrectionFactor: config.GpobaseCorrectionFactor,
eth.ApiBackend = &EthApiBackend{eth, nil}
gpoParams := gasprice.Config{
Blocks: config.GpoBlocks,
Percentile: config.GpoPercentile,
Default: config.GasPrice,
}
gpo := gasprice.NewGasPriceOracle(eth.blockchain, chainDb, eth.eventMux, gpoParams)
eth.ApiBackend = &EthApiBackend{eth, gpo}
eth.ApiBackend.gpo = gasprice.NewOracle(eth.ApiBackend, gpoParams)
return eth, nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The go-ethereum Authors
// Copyright 2016 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
@ -17,212 +17,158 @@
package gasprice
import (
"context"
"math/big"
"math/rand"
"sort"
"sync"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
)
const (
gpoProcessPastBlocks = 100
var maxPrice = big.NewInt(500 * params.Shannon)
// for testing
gpoDefaultBaseCorrectionFactor = 110
gpoDefaultMinGasPrice = 10000000000000
)
type blockPriceInfo struct {
baseGasPrice *big.Int
type Config struct {
Blocks int
Percentile int
Default *big.Int
}
type GpoParams struct {
GpoMinGasPrice *big.Int
GpoMaxGasPrice *big.Int
GpoFullBlockRatio int
GpobaseStepDown int
GpobaseStepUp int
GpobaseCorrectionFactor int
// 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
}
// GasPriceOracle recommends gas prices based on the content of recent
// blocks.
type GasPriceOracle struct {
chain *core.BlockChain
db ethdb.Database
evmux *event.TypeMux
params *GpoParams
initOnce sync.Once
minPrice *big.Int
lastBaseMutex sync.Mutex
lastBase *big.Int
// state of listenLoop
blocks map[uint64]*blockPriceInfo
firstProcessed, lastProcessed uint64
minBase *big.Int
}
// NewGasPriceOracle returns a new oracle.
func NewGasPriceOracle(chain *core.BlockChain, db ethdb.Database, evmux *event.TypeMux, params *GpoParams) *GasPriceOracle {
minprice := params.GpoMinGasPrice
if minprice == nil {
minprice = big.NewInt(gpoDefaultMinGasPrice)
// NewOracle returns a new oracle.
func NewOracle(backend ethapi.Backend, params Config) *Oracle {
blocks := params.Blocks
if blocks < 1 {
blocks = 1
}
minbase := new(big.Int).Mul(minprice, big.NewInt(100))
if params.GpobaseCorrectionFactor > 0 {
minbase = minbase.Div(minbase, big.NewInt(int64(params.GpobaseCorrectionFactor)))
percent := params.Percentile
if percent < 0 {
percent = 0
}
return &GasPriceOracle{
chain: chain,
db: db,
evmux: evmux,
params: params,
blocks: make(map[uint64]*blockPriceInfo),
minBase: minbase,
minPrice: minprice,
lastBase: minprice,
if percent > 100 {
percent = 100
}
}
func (gpo *GasPriceOracle) init() {
gpo.initOnce.Do(func() {
gpo.processPastBlocks()
go gpo.listenLoop()
})
}
func (self *GasPriceOracle) processPastBlocks() {
last := int64(-1)
cblock := self.chain.CurrentBlock()
if cblock != nil {
last = int64(cblock.NumberU64())
return &Oracle{
backend: backend,
lastPrice: params.Default,
checkBlocks: blocks,
maxEmpty: blocks / 2,
maxBlocks: blocks * 5,
percentile: percent,
}
first := int64(0)
if last > gpoProcessPastBlocks {
first = last - gpoProcessPastBlocks
}
self.firstProcessed = uint64(first)
for i := first; i <= last; i++ {
block := self.chain.GetBlockByNumber(uint64(i))
if block != nil {
self.processBlock(block)
}
}
}
func (self *GasPriceOracle) listenLoop() {
events := self.evmux.Subscribe(core.ChainEvent{}, core.ChainSplitEvent{})
defer events.Unsubscribe()
for event := range events.Chan() {
switch event := event.Data.(type) {
case core.ChainEvent:
self.processBlock(event.Block)
case core.ChainSplitEvent:
self.processBlock(event.Block)
}
}
}
func (self *GasPriceOracle) processBlock(block *types.Block) {
i := block.NumberU64()
if i > self.lastProcessed {
self.lastProcessed = i
}
lastBase := self.minPrice
bpl := self.blocks[i-1]
if bpl != nil {
lastBase = bpl.baseGasPrice
}
if lastBase == nil {
return
}
var corr int
lp := self.lowestPrice(block)
if lp == nil {
return
}
if lastBase.Cmp(lp) < 0 {
corr = self.params.GpobaseStepUp
} else {
corr = -self.params.GpobaseStepDown
}
crand := int64(corr * (900 + rand.Intn(201)))
newBase := new(big.Int).Mul(lastBase, big.NewInt(1000000+crand))
newBase.Div(newBase, big.NewInt(1000000))
if newBase.Cmp(self.minBase) < 0 {
newBase = self.minBase
}
bpi := self.blocks[i]
if bpi == nil {
bpi = &blockPriceInfo{}
self.blocks[i] = bpi
}
bpi.baseGasPrice = newBase
self.lastBaseMutex.Lock()
self.lastBase = newBase
self.lastBaseMutex.Unlock()
log.Trace("Processed block, base price updated", "number", i, "base", newBase)
}
// returns the lowers possible price with which a tx was or could have been included
func (self *GasPriceOracle) lowestPrice(block *types.Block) *big.Int {
gasUsed := big.NewInt(0)
receipts := core.GetBlockReceipts(self.db, block.Hash(), block.NumberU64())
if len(receipts) > 0 {
if cgu := receipts[len(receipts)-1].CumulativeGasUsed; cgu != nil {
gasUsed = receipts[len(receipts)-1].CumulativeGasUsed
}
}
if new(big.Int).Mul(gasUsed, big.NewInt(100)).Cmp(new(big.Int).Mul(block.GasLimit(),
big.NewInt(int64(self.params.GpoFullBlockRatio)))) < 0 {
// block is not full, could have posted a tx with MinGasPrice
return big.NewInt(0)
}
txs := block.Transactions()
if len(txs) == 0 {
return big.NewInt(0)
}
// block is full, find smallest gasPrice
minPrice := txs[0].GasPrice()
for i := 1; i < len(txs); i++ {
price := txs[i].GasPrice()
if price.Cmp(minPrice) < 0 {
minPrice = price
}
}
return minPrice
}
// SuggestPrice returns the recommended gas price.
func (self *GasPriceOracle) SuggestPrice() *big.Int {
self.init()
self.lastBaseMutex.Lock()
price := new(big.Int).Set(self.lastBase)
self.lastBaseMutex.Unlock()
func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
gpo.cacheLock.RLock()
lastHead := gpo.lastHead
lastPrice := gpo.lastPrice
gpo.cacheLock.RUnlock()
price.Mul(price, big.NewInt(int64(self.params.GpobaseCorrectionFactor)))
price.Div(price, big.NewInt(100))
if price.Cmp(self.minPrice) < 0 {
price.Set(self.minPrice)
} else if self.params.GpoMaxGasPrice != nil && price.Cmp(self.params.GpoMaxGasPrice) > 0 {
price.Set(self.params.GpoMaxGasPrice)
head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash()
if headHash == lastHead {
return lastPrice, nil
}
return price
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
}
blockNum := head.Number.Uint64()
ch := make(chan getBlockPricesResult, gpo.checkBlocks)
sent := 0
exp := 0
var txPrices []*big.Int
for sent < gpo.checkBlocks && blockNum > 0 {
go gpo.getBlockPrices(ctx, blockNum, ch)
sent++
exp++
blockNum--
}
maxEmpty := gpo.maxEmpty
for exp > 0 {
res := <-ch
if res.err != nil {
return lastPrice, res.err
}
exp--
if len(res.prices) > 0 {
txPrices = append(txPrices, res.prices...)
continue
}
if maxEmpty > 0 {
maxEmpty--
continue
}
if blockNum > 0 && sent < gpo.maxBlocks {
go gpo.getBlockPrices(ctx, blockNum, ch)
sent++
exp++
blockNum--
}
}
price := 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
gpo.cacheLock.Unlock()
return price, nil
}
type getBlockPricesResult struct {
prices []*big.Int
err error
}
// getLowestPrice 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, blockNum uint64, ch chan getBlockPricesResult) {
block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
ch <- getBlockPricesResult{nil, err}
return
}
txs := block.Transactions()
prices := make([]*big.Int, len(txs))
for i, tx := range txs {
prices[i] = tx.GasPrice()
}
ch <- getBlockPricesResult{prices, nil}
}
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] }

View File

@ -1,160 +0,0 @@
// Copyright 2016 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 <http://www.gnu.org/licenses/>.
package gasprice
import (
"context"
"math/big"
"sort"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/rpc"
)
const (
LpoAvgCount = 5
LpoMinCount = 3
LpoMaxBlocks = 20
LpoSelect = 50
LpoDefaultPrice = 20000000000
)
// LightPriceOracle recommends gas prices based on the content of recent
// blocks. Suitable for both light and full clients.
type LightPriceOracle struct {
backend ethapi.Backend
lastHead common.Hash
lastPrice *big.Int
cacheLock sync.RWMutex
fetchLock sync.Mutex
}
// NewLightPriceOracle returns a new oracle.
func NewLightPriceOracle(backend ethapi.Backend) *LightPriceOracle {
return &LightPriceOracle{
backend: backend,
lastPrice: big.NewInt(LpoDefaultPrice),
}
}
// SuggestPrice returns the recommended gas price.
func (self *LightPriceOracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
self.cacheLock.RLock()
lastHead := self.lastHead
lastPrice := self.lastPrice
self.cacheLock.RUnlock()
head, _ := self.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash()
if headHash == lastHead {
return lastPrice, nil
}
self.fetchLock.Lock()
defer self.fetchLock.Unlock()
// try checking the cache again, maybe the last fetch fetched what we need
self.cacheLock.RLock()
lastHead = self.lastHead
lastPrice = self.lastPrice
self.cacheLock.RUnlock()
if headHash == lastHead {
return lastPrice, nil
}
blockNum := head.Number.Uint64()
chn := make(chan lpResult, LpoMaxBlocks)
sent := 0
exp := 0
var lps bigIntArray
for sent < LpoAvgCount && blockNum > 0 {
go self.getLowestPrice(ctx, blockNum, chn)
sent++
exp++
blockNum--
}
maxEmpty := LpoAvgCount - LpoMinCount
for exp > 0 {
res := <-chn
if res.err != nil {
return nil, res.err
}
exp--
if res.price != nil {
lps = append(lps, res.price)
} else {
if maxEmpty > 0 {
maxEmpty--
} else {
if blockNum > 0 && sent < LpoMaxBlocks {
go self.getLowestPrice(ctx, blockNum, chn)
sent++
exp++
blockNum--
}
}
}
}
price := lastPrice
if len(lps) > 0 {
sort.Sort(lps)
price = lps[(len(lps)-1)*LpoSelect/100]
}
self.cacheLock.Lock()
self.lastHead = headHash
self.lastPrice = price
self.cacheLock.Unlock()
return price, nil
}
type lpResult struct {
price *big.Int
err error
}
// getLowestPrice 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 (self *LightPriceOracle) getLowestPrice(ctx context.Context, blockNum uint64, chn chan lpResult) {
block, err := self.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
chn <- lpResult{nil, err}
return
}
txs := block.Transactions()
if len(txs) == 0 {
chn <- lpResult{nil, nil}
return
}
// find smallest gasPrice
minPrice := txs[0].GasPrice()
for i := 1; i < len(txs); i++ {
price := txs[i].GasPrice()
if price.Cmp(minPrice) < 0 {
minPrice = price
}
}
chn <- lpResult{minPrice, nil}
}
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] }

View File

@ -38,7 +38,7 @@ import (
type LesApiBackend struct {
eth *LightEthereum
gpo *gasprice.LightPriceOracle
gpo *gasprice.Oracle
}
func (b *LesApiBackend) ChainConfig() *params.ChainConfig {

View File

@ -111,7 +111,12 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
relay.reqDist = eth.protocolManager.reqDist
eth.ApiBackend = &LesApiBackend{eth, nil}
eth.ApiBackend.gpo = gasprice.NewLightPriceOracle(eth.ApiBackend)
gpoParams := gasprice.Config{
Blocks: config.GpoBlocks,
Percentile: config.GpoPercentile,
Default: config.GasPrice,
}
eth.ApiBackend.gpo = gasprice.NewOracle(eth.ApiBackend, gpoParams)
return eth, nil
}

View File

@ -168,20 +168,16 @@ func NewNode(datadir string, config *NodeConfig) (stack *Node, _ error) {
// Register the Ethereum protocol if requested
if config.EthereumEnabled {
ethConf := &eth.Config{
Genesis: genesis,
LightMode: true,
DatabaseCache: config.EthereumDatabaseCache,
NetworkId: config.EthereumNetworkID,
GasPrice: new(big.Int).SetUint64(20 * params.Shannon),
GpoMinGasPrice: new(big.Int).SetUint64(50 * params.Shannon),
GpoMaxGasPrice: new(big.Int).SetUint64(500 * params.Shannon),
GpoFullBlockRatio: 80,
GpobaseStepDown: 10,
GpobaseStepUp: 100,
GpobaseCorrectionFactor: 110,
EthashCacheDir: "ethash",
EthashCachesInMem: 2,
EthashCachesOnDisk: 3,
Genesis: genesis,
LightMode: true,
DatabaseCache: config.EthereumDatabaseCache,
NetworkId: config.EthereumNetworkID,
GasPrice: new(big.Int).SetUint64(20 * params.Shannon),
GpoBlocks: 5,
GpoPercentile: 50,
EthashCacheDir: "ethash",
EthashCachesInMem: 2,
EthashCachesOnDisk: 3,
}
if err := rawStack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return les.New(ctx, ethConf)