diff --git a/eth/backend.go b/eth/backend.go index bda307d95b..6fbf00d66b 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -18,6 +18,7 @@ package eth import ( + "context" "errors" "fmt" "math/big" @@ -215,18 +216,23 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock) eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) - eth.APIBackend = &EthAPIBackend{ctx.ExtRPCEnabled(), eth, nil} gpoParams := config.GPO if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } - eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams) - + eth.APIBackend = &EthAPIBackend{ + ctx.ExtRPCEnabled(), eth, + gasprice.NewOracle(gpoParams, chainConfig, + func() *types.Header { return eth.blockchain.CurrentHeader() }, + func(ctx context.Context, number uint64) (*types.Block, error) { + return eth.blockchain.GetBlockByNumber(number), nil + }, + ), + } eth.dialCandiates, err = eth.setupDiscovery(&ctx.Config.P2P) if err != nil { return nil, err } - return eth, nil } diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 3b8db78a1c..71e5339715 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -24,9 +24,7 @@ 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" ) var maxPrice = big.NewInt(500 * params.GWei) @@ -37,26 +35,36 @@ type Config struct { Default *big.Int `toml:",omitempty"` } +type ( + // headHeaderRetrieverFn is a callback type for retrieving the head header from the local chain. + headHeaderRetrieverFn func() *types.Header + + // blockRetrieverFn is a callback type for retrieving a block from the local chain or p2p network. + blockRetrieverFn func(context.Context, uint64) (*types.Block, error) +) + // 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 + lastHead common.Hash + lastPrice *big.Int + cacheLock sync.RWMutex + fetchLock sync.Mutex + chainConfig *params.ChainConfig + getHeadHeader headHeaderRetrieverFn + getBlock blockRetrieverFn checkBlocks, maxEmpty, maxBlocks int percentile int } // NewOracle returns a new oracle. -func NewOracle(backend ethapi.Backend, params Config) *Oracle { - blocks := params.Blocks +func NewOracle(config Config, chainConfig *params.ChainConfig, getHeadHeader headHeaderRetrieverFn, getBlock blockRetrieverFn) *Oracle { + blocks := config.Blocks if blocks < 1 { blocks = 1 } - percent := params.Percentile + percent := config.Percentile if percent < 0 { percent = 0 } @@ -64,12 +72,14 @@ 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, + lastPrice: config.Default, + checkBlocks: blocks, + maxEmpty: blocks / 2, + maxBlocks: blocks * 5, + percentile: percent, + chainConfig: chainConfig, + getHeadHeader: getHeadHeader, + getBlock: getBlock, } } @@ -80,7 +90,7 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { lastPrice := gpo.lastPrice gpo.cacheLock.RUnlock() - head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) + head := gpo.getHeadHeader() headHash := head.Hash() if headHash == lastHead { return lastPrice, nil @@ -104,7 +114,7 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { 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) + go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.chainConfig, big.NewInt(int64(blockNum))), blockNum, ch) sent++ exp++ blockNum-- @@ -125,7 +135,7 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { continue } if blockNum > 0 && sent < gpo.maxBlocks { - go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch) + go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.chainConfig, big.NewInt(int64(blockNum))), blockNum, ch) sent++ exp++ blockNum-- @@ -161,7 +171,7 @@ func (t transactionsByGasPrice) Less(i, j int) bool { return t[i].GasPrice().Cmp // 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) { - block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum)) + block, err := gpo.getBlock(ctx, blockNum) if block == nil { ch <- getBlockPricesResult{nil, err} return diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go new file mode 100644 index 0000000000..5a534364af --- /dev/null +++ b/eth/gasprice/gasprice_test.go @@ -0,0 +1,99 @@ +// 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" +) + +type testBackend struct { + chain *core.BlockChain +} + +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: 50, + Default: big.NewInt(params.GWei), + } + backend := newTestBackend(t) + oracle := NewOracle(config, params.TestChainConfig, func() *types.Header { return backend.CurrentHeader() }, func(ctx context.Context, number uint64) (*types.Block, error) { + return backend.GetBlockByNumber(number), nil + }) + 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) + } +} diff --git a/les/client.go b/les/client.go index 36d5a8d2fe..fb6c3f5075 100644 --- a/les/client.go +++ b/les/client.go @@ -142,14 +142,11 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { leth.blockchain.SetHead(compat.RewindTo) rawdb.WriteChainConfig(chainDb, genesisHash, chainConfig) } - - leth.ApiBackend = &LesApiBackend{ctx.ExtRPCEnabled(), leth, nil} gpoParams := config.GPO if gpoParams.Default == nil { gpoParams.Default = config.Miner.GasPrice } - leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) - + leth.ApiBackend = &LesApiBackend{ctx.ExtRPCEnabled(), leth, gasprice.NewOracle(gpoParams, chainConfig, func() *types.Header { return leth.blockchain.CurrentHeader() }, leth.blockchain.GetBlockByNumber)} return leth, nil }