diff --git a/api/api_ethereum.go b/api/api_ethereum.go
index 0184898150..0b58788ed7 100644
--- a/api/api_ethereum.go
+++ b/api/api_ethereum.go
@@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"math/big"
+ "strconv"
"strings"
"sync/atomic"
"time"
@@ -314,7 +315,7 @@ func (api *EthereumAPI) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big,
return api.publicKlayAPI.GasPrice(ctx)
}
-type feeHistoryResult struct {
+type FeeHistoryResult struct {
OldestBlock *hexutil.Big `json:"oldestBlock"`
Reward [][]*hexutil.Big `json:"reward,omitempty"`
BaseFee []*hexutil.Big `json:"baseFeePerGas,omitempty"`
@@ -324,9 +325,49 @@ type feeHistoryResult struct {
// DecimalOrHex unmarshals a non-negative decimal or hex parameter into a uint64.
type DecimalOrHex uint64
-func (api *EthereumAPI) FeeHistory(ctx context.Context, blockCount DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*feeHistoryResult, error) {
- // TODO-Klaytn: Not implemented yet.
- return nil, nil
+// UnmarshalJSON implements json.Unmarshaler.
+func (dh *DecimalOrHex) UnmarshalJSON(data []byte) error {
+ input := strings.TrimSpace(string(data))
+ if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
+ input = input[1 : len(input)-1]
+ }
+
+ value, err := strconv.ParseUint(input, 10, 64)
+ if err != nil {
+ value, err = hexutil.DecodeUint64(input)
+ }
+ if err != nil {
+ return err
+ }
+ *dh = DecimalOrHex(value)
+ return nil
+}
+
+func (api *EthereumAPI) FeeHistory(ctx context.Context, blockCount DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*FeeHistoryResult, error) {
+ oldest, reward, baseFee, gasUsed, err := api.publicKlayAPI.b.FeeHistory(ctx, int(blockCount), lastBlock, rewardPercentiles)
+ if err != nil {
+ return nil, err
+ }
+ results := &FeeHistoryResult{
+ OldestBlock: (*hexutil.Big)(oldest),
+ GasUsedRatio: gasUsed,
+ }
+ if reward != nil {
+ results.Reward = make([][]*hexutil.Big, len(reward))
+ for i, w := range reward {
+ results.Reward[i] = make([]*hexutil.Big, len(w))
+ for j, v := range w {
+ results.Reward[i][j] = (*hexutil.Big)(v)
+ }
+ }
+ }
+ if baseFee != nil {
+ results.BaseFee = make([]*hexutil.Big, len(baseFee))
+ for i, v := range baseFee {
+ results.BaseFee[i] = (*hexutil.Big)(v)
+ }
+ }
+ return results, nil
}
// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not
@@ -1310,7 +1351,7 @@ func EthDoCall(ctx context.Context, b Backend, args EthTransactionArgs, blockNrO
// TODO-Klaytn: Klaytn is using fixed baseFee as now but, if we change this fixed baseFee as dynamic baseFee, we should update this logic too.
fixedBaseFee := new(big.Int).SetUint64(params.BaseFee)
- intrinsicGas, err := types.IntrinsicGas(args.data(), args.To == nil, b.ChainConfig().Rules(header.Number))
+ intrinsicGas, err := types.IntrinsicGas(args.data(), nil, args.To == nil, b.ChainConfig().Rules(header.Number))
if err != nil {
return nil, 0, err
}
diff --git a/api/backend.go b/api/backend.go
index f478de3639..000de1b49d 100644
--- a/api/backend.go
+++ b/api/backend.go
@@ -51,6 +51,7 @@ type Backend interface {
AccountManager() accounts.AccountManager
RPCGasCap() *big.Int // global gas cap for klay_call over rpc: DoS protection
Engine() consensus.Engine
+ FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error)
// BlockChain API
SetHead(number uint64)
diff --git a/api/mocks/backend_mock.go b/api/mocks/backend_mock.go
index 28f2a3d149..0946b1135c 100644
--- a/api/mocks/backend_mock.go
+++ b/api/mocks/backend_mock.go
@@ -176,6 +176,24 @@ func (mr *MockBackendMockRecorder) EventMux() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EventMux", reflect.TypeOf((*MockBackend)(nil).EventMux))
}
+// FeeHistory mocks base method.
+func (m *MockBackend) FeeHistory(arg0 context.Context, arg1 int, arg2 rpc.BlockNumber, arg3 []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FeeHistory", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*big.Int)
+ ret1, _ := ret[1].([][]*big.Int)
+ ret2, _ := ret[2].([]*big.Int)
+ ret3, _ := ret[3].([]float64)
+ ret4, _ := ret[4].(error)
+ return ret0, ret1, ret2, ret3, ret4
+}
+
+// FeeHistory indicates an expected call of FeeHistory.
+func (mr *MockBackendMockRecorder) FeeHistory(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FeeHistory", reflect.TypeOf((*MockBackend)(nil).FeeHistory), arg0, arg1, arg2, arg3)
+}
+
// GetBlockReceipts mocks base method.
func (m *MockBackend) GetBlockReceipts(arg0 context.Context, arg1 common.Hash) types.Receipts {
m.ctrl.T.Helper()
diff --git a/log/log_modules.go b/log/log_modules.go
index 634ee38678..75347ed90c 100644
--- a/log/log_modules.go
+++ b/log/log_modules.go
@@ -124,6 +124,7 @@ const (
ChainDataFetcher
KAS
FORK
+ NodeCnGasPrice
// ModuleNameLen should be placed at the end of the list.
ModuleNameLen
@@ -199,4 +200,5 @@ var moduleNames = [ModuleNameLen]string{
"datasync/chaindatafetcher",
"kas",
"fork",
+ "node/cn/gasprice",
}
diff --git a/node/cn/api_backend.go b/node/cn/api_backend.go
index 007c0192cc..575baa8b40 100644
--- a/node/cn/api_backend.go
+++ b/node/cn/api_backend.go
@@ -318,3 +318,7 @@ func (b *CNAPIBackend) RPCGasCap() *big.Int {
func (b *CNAPIBackend) Engine() consensus.Engine {
return b.cn.engine
}
+
+func (b *CNAPIBackend) FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error) {
+ return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles)
+}
diff --git a/node/cn/config.go b/node/cn/config.go
index 38b21deb01..df3c218569 100644
--- a/node/cn/config.go
+++ b/node/cn/config.go
@@ -57,8 +57,10 @@ func GetDefaultConfig() *Config {
TxPool: blockchain.DefaultTxPoolConfig,
GPO: gasprice.Config{
- Blocks: 20,
- Percentile: 60,
+ Blocks: 20,
+ Percentile: 60,
+ MaxHeaderHistory: 1024,
+ MaxBlockHistory: 1024,
},
WsEndpoint: "localhost:8546",
diff --git a/node/cn/gasprice/feehistory.go b/node/cn/gasprice/feehistory.go
new file mode 100644
index 0000000000..dec105d32e
--- /dev/null
+++ b/node/cn/gasprice/feehistory.go
@@ -0,0 +1,267 @@
+// Modifications Copyright 2022 The Klaytn Authors
+// Copyright 2021 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 .
+//
+// This file is derived from eth/gasprice/feehistory.go (2021/11/09).
+// Modified and improved for the klaytn development.
+
+package gasprice
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math/big"
+ "sort"
+ "sync/atomic"
+
+ "github.com/klaytn/klaytn/blockchain/types"
+ "github.com/klaytn/klaytn/common"
+ "github.com/klaytn/klaytn/log"
+ "github.com/klaytn/klaytn/networks/rpc"
+ "github.com/klaytn/klaytn/params"
+)
+
+var (
+ logger = log.NewModuleLogger(log.NodeCnGasPrice)
+ errInvalidPercentile = errors.New("invalid reward percentile")
+ errRequestBeyondHead = errors.New("request beyond head block")
+)
+
+const (
+ // maxBlockFetchers is the max number of goroutines to spin up to pull blocks
+ // for the fee history calculation.
+ maxBlockFetchers = 1
+)
+
+// blockFees represents a single block for processing
+type blockFees struct {
+ // set by the caller
+ blockNumber uint64
+ header *types.Header
+ block *types.Block // only set if reward percentiles are requested
+ receipts types.Receipts
+ // filled by processBlock
+ results processedFees
+ err error
+}
+
+// processedFees contains the results of a processed block and is also used for caching
+type processedFees struct {
+ reward []*big.Int
+ baseFee, nextBaseFee *big.Int
+ gasUsedRatio float64
+}
+
+// txGasAndReward is sorted in ascending order based on reward
+type (
+ txGasAndReward struct {
+ gasUsed uint64
+ reward *big.Int
+ }
+ sortGasAndReward []txGasAndReward
+)
+
+func (s sortGasAndReward) Len() int { return len(s) }
+func (s sortGasAndReward) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+func (s sortGasAndReward) Less(i, j int) bool {
+ return s[i].reward.Cmp(s[j].reward) < 0
+}
+
+// processBlock takes a blockFees structure with the blockNumber, the header and optionally
+// the block field filled in, retrieves the block from the backend if not present yet and
+// fills in the rest of the fields.
+func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
+ chainconfig := oracle.backend.ChainConfig()
+ // TODO-Klaytn: If we implement baseFee feature like Ethereum does, we should set it from header, not constant.
+ bf.results.baseFee = new(big.Int).SetUint64(params.BaseFee)
+ // TODO-Klaytn: If we implement baseFee feature like Ethereum does, we should calculate nextBaseFee from parent block header.
+ bf.results.nextBaseFee = new(big.Int).SetUint64(params.BaseFee)
+ // There is no GasLimit in Klaytn, so it is enough to use pre-defined constant in api package as now.
+ bf.results.gasUsedRatio = float64(bf.header.GasUsed) / float64(params.UpperGasLimit)
+ if len(percentiles) == 0 {
+ // rewards were not requested, return null
+ return
+ }
+ if bf.block == nil || (bf.receipts == nil && len(bf.block.Transactions()) != 0) {
+ logger.Error("Block or receipts are missing while reward percentiles are requested")
+ return
+ }
+
+ bf.results.reward = make([]*big.Int, len(percentiles))
+ if len(bf.block.Transactions()) == 0 {
+ // return an all zero row if there are no transactions to gather data from
+ for i := range bf.results.reward {
+ bf.results.reward[i] = new(big.Int)
+ }
+ return
+ }
+
+ sorter := make(sortGasAndReward, len(bf.block.Transactions()))
+ for i := range bf.block.Transactions() {
+ // TODO-Klaytn: If we change the fixed unit price policy and add baseFee feature, we should re-calculate reward.
+ reward := new(big.Int).SetUint64(chainconfig.UnitPrice) // As now, reward is always same because the baseFee in Klaytn is 0 and gasPrice is fixed.
+ sorter[i] = txGasAndReward{gasUsed: bf.receipts[i].GasUsed, reward: reward}
+ }
+ sort.Sort(sorter)
+
+ var txIndex int
+ sumGasUsed := sorter[0].gasUsed
+
+ for i, p := range percentiles {
+ thresholdGasUsed := uint64(float64(bf.block.GasUsed()) * p / 100)
+ for sumGasUsed < thresholdGasUsed && txIndex < len(bf.block.Transactions())-1 {
+ txIndex++
+ sumGasUsed += sorter[txIndex].gasUsed
+ }
+ bf.results.reward[i] = sorter[txIndex].reward
+ }
+}
+
+// resolveBlockRange resolves the specified block range to absolute block numbers while also
+// enforcing backend specific limitations.
+// Pending block does not exist in Klaytn, so there is no logic to look up pending blocks.
+// This part has a different implementation with Ethereum.
+// Note: an error is only returned if retrieving the head header has failed. If there are no
+// retrievable blocks in the specified range then zero block count is returned with no error.
+func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.BlockNumber, blocks int) (uint64, int, error) {
+ var headBlock rpc.BlockNumber
+ // query either pending block or head header and set headBlock
+ if lastBlock == rpc.PendingBlockNumber {
+ // pending block not supported by backend, process until latest block
+ lastBlock = rpc.LatestBlockNumber
+ blocks--
+ if blocks == 0 {
+ return 0, 0, nil
+ }
+ }
+ // if pending block is not fetched then we retrieve the head header to get the head block number
+ if latestHeader, err := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber); err == nil {
+ headBlock = rpc.BlockNumber(latestHeader.Number.Uint64())
+ } else {
+ return 0, 0, err
+ }
+ if lastBlock == rpc.LatestBlockNumber {
+ lastBlock = headBlock
+ } else if lastBlock > headBlock {
+ return 0, 0, fmt.Errorf("%w: requested %d, head %d", errRequestBeyondHead, lastBlock, headBlock)
+ }
+ // ensure not trying to retrieve before genesis
+ if rpc.BlockNumber(blocks) > lastBlock+1 {
+ blocks = int(lastBlock + 1)
+ }
+ return uint64(lastBlock), blocks, nil
+}
+
+// FeeHistory returns data relevant for fee estimation based on the specified range of blocks.
+// The range can be specified either with absolute block numbers or ending with the latest
+// or pending block. Backends may or may not support gathering data from the pending block
+// or blocks older than a certain age (specified in maxHistory). The first block of the
+// actually processed range is returned to avoid ambiguity when parts of the requested range
+// are not available or when the head has changed during processing this request.
+// Three arrays are returned based on the processed blocks:
+// - reward: the requested percentiles of effective priority fees per gas of transactions in each
+// block, sorted in ascending order and weighted by gas used.
+// - baseFee: base fee per gas in the given block
+// - gasUsedRatio: gasUsed/gasLimit in the given block
+// Note: baseFee includes the next block after the newest of the returned range, because this
+// value can be derived from the newest block.
+func (oracle *Oracle) FeeHistory(
+ ctx context.Context, blocks int,
+ unresolvedLastBlock rpc.BlockNumber,
+ rewardPercentiles []float64,
+) (*big.Int, [][]*big.Int, []*big.Int, []float64, error) {
+ if blocks < 1 {
+ return common.Big0, nil, nil, nil, nil // returning with no data and no error means there are no retrievable blocks
+ }
+ maxFeeHistory := oracle.maxHeaderHistory
+ if len(rewardPercentiles) != 0 {
+ maxFeeHistory = oracle.maxBlockHistory
+ }
+ if blocks > maxFeeHistory {
+ logger.Warn("Sanitizing fee history length", "requested", blocks, "truncated", maxFeeHistory)
+ blocks = maxFeeHistory
+ }
+ for i, p := range rewardPercentiles {
+ if p < 0 || p > 100 {
+ return common.Big0, nil, nil, nil, fmt.Errorf("%w: %f", errInvalidPercentile, p)
+ }
+ if i > 0 && p < rewardPercentiles[i-1] {
+ return common.Big0, nil, nil, nil, fmt.Errorf("%w: #%d:%f > #%d:%f", errInvalidPercentile, i-1, rewardPercentiles[i-1], i, p)
+ }
+ }
+ var err error
+ lastBlock, blocks, err := oracle.resolveBlockRange(ctx, unresolvedLastBlock, blocks)
+ if err != nil || blocks == 0 {
+ return common.Big0, nil, nil, nil, err
+ }
+ oldestBlock := lastBlock + 1 - uint64(blocks)
+
+ var (
+ next = oldestBlock
+ results = make(chan *blockFees, blocks)
+ )
+ for i := 0; i < maxBlockFetchers && i < blocks; i++ {
+ go func() {
+ for {
+ // Retrieve the next block number to fetch with this goroutine
+ blockNumber := atomic.AddUint64(&next, 1) - 1
+ if blockNumber > lastBlock {
+ return
+ }
+
+ fees := &blockFees{blockNumber: blockNumber}
+ if len(rewardPercentiles) != 0 {
+ fees.block, fees.err = oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNumber))
+ if fees.block != nil && fees.err == nil {
+ fees.receipts = oracle.backend.GetBlockReceipts(ctx, fees.block.Hash())
+ fees.header = fees.block.Header()
+ }
+ } else {
+ fees.header, fees.err = oracle.backend.HeaderByNumber(ctx, rpc.BlockNumber(blockNumber))
+ }
+ if fees.header != nil && fees.err == nil {
+ oracle.processBlock(fees, rewardPercentiles)
+ }
+ // send to results even if empty to guarantee that blocks items are sent in total
+ results <- fees
+ }
+ }()
+ }
+ var (
+ reward = make([][]*big.Int, blocks)
+ baseFee = make([]*big.Int, blocks+1)
+ gasUsedRatio = make([]float64, blocks)
+ blockCount = blocks
+ )
+ for ; blocks > 0; blocks-- {
+ fees := <-results
+ if fees.err != nil {
+ return common.Big0, nil, nil, nil, fees.err
+ }
+ i := int(fees.blockNumber - oldestBlock)
+ reward[i], baseFee[i], baseFee[i+1], gasUsedRatio[i] = fees.results.reward, fees.results.baseFee, fees.results.nextBaseFee, fees.results.gasUsedRatio
+ }
+ if len(rewardPercentiles) != 0 {
+ reward = reward[:blockCount]
+ } else {
+ reward = nil
+ }
+ baseFee, gasUsedRatio = baseFee[:blockCount+1], gasUsedRatio[:blockCount]
+ return new(big.Int).SetUint64(oldestBlock), reward, baseFee, gasUsedRatio, nil
+}
diff --git a/node/cn/gasprice/feehistory_test.go b/node/cn/gasprice/feehistory_test.go
new file mode 100644
index 0000000000..f00bb387c9
--- /dev/null
+++ b/node/cn/gasprice/feehistory_test.go
@@ -0,0 +1,89 @@
+// Modifications Copyright 2022 The Klaytn Authors
+// Copyright 2021 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 .
+//
+// This file is derived from eth/gasprice/feehistory.go (2021/11/09).
+// Modified and improved for the klaytn development
+
+package gasprice
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/klaytn/klaytn/networks/rpc"
+)
+
+func TestFeeHistory(t *testing.T) {
+ var cases = []struct {
+ maxHeader, maxBlock int
+ count int
+ last rpc.BlockNumber
+ percent []float64
+ expFirst uint64
+ expCount int
+ expErr error
+ }{
+ {1000, 1000, 10, 30, nil, 21, 10, nil},
+ {1000, 1000, 10, 30, []float64{0, 10}, 21, 10, nil},
+ {1000, 1000, 10, 30, []float64{20, 10}, 0, 0, errInvalidPercentile},
+ {1000, 1000, 1000000000, 30, nil, 0, 31, nil},
+ {1000, 1000, 1000000000, rpc.LatestBlockNumber, nil, 0, 33, nil},
+ {1000, 1000, 10, 40, nil, 0, 0, errRequestBeyondHead},
+ {1000, 1000, 10, 40, nil, 0, 0, errRequestBeyondHead},
+ {20, 2, 100, rpc.LatestBlockNumber, nil, 13, 20, nil},
+ {20, 2, 100, rpc.LatestBlockNumber, []float64{0, 10}, 31, 2, nil},
+ {20, 2, 100, 32, []float64{0, 10}, 31, 2, nil},
+ {1000, 1000, 1, rpc.PendingBlockNumber, nil, 0, 0, nil},
+ {1000, 1000, 2, rpc.PendingBlockNumber, nil, 32, 1, nil},
+ }
+ for i, c := range cases {
+ config := Config{
+ MaxHeaderHistory: c.maxHeader,
+ MaxBlockHistory: c.maxBlock,
+ }
+ backend := newTestBackend(t)
+ oracle := NewOracle(backend, config)
+
+ first, reward, baseFee, ratio, err := oracle.FeeHistory(context.Background(), c.count, c.last, c.percent)
+
+ expReward := c.expCount
+ if len(c.percent) == 0 {
+ expReward = 0
+ }
+ expBaseFee := c.expCount
+ if expBaseFee != 0 {
+ expBaseFee++
+ }
+
+ if first.Uint64() != c.expFirst {
+ t.Fatalf("Test case %d: first block mismatch, want %d, got %d", i, c.expFirst, first)
+ }
+ if len(reward) != expReward {
+ t.Fatalf("Test case %d: reward array length mismatch, want %d, got %d", i, expReward, len(reward))
+ }
+ if len(baseFee) != expBaseFee {
+ t.Fatalf("Test case %d: baseFee array length mismatch, want %d, got %d", i, expBaseFee, len(baseFee))
+ }
+ if len(ratio) != c.expCount {
+ t.Fatalf("Test case %d: gasUsedRatio array length mismatch, want %d, got %d", i, c.expCount, len(ratio))
+ }
+ if err != c.expErr && !errors.Is(err, c.expErr) {
+ t.Fatalf("Test case %d: error mismatch, want %v, got %v", i, c.expErr, err)
+ }
+ }
+}
diff --git a/node/cn/gasprice/gasprice.go b/node/cn/gasprice/gasprice.go
index cc67211ae3..1e25696695 100644
--- a/node/cn/gasprice/gasprice.go
+++ b/node/cn/gasprice/gasprice.go
@@ -25,7 +25,9 @@ import (
"math/big"
"sync"
- "github.com/klaytn/klaytn/api"
+ "github.com/klaytn/klaytn/blockchain/types"
+ "github.com/klaytn/klaytn/networks/rpc"
+
"github.com/klaytn/klaytn/common"
"github.com/klaytn/klaytn/params"
)
@@ -33,26 +35,37 @@ import (
var maxPrice = big.NewInt(500 * params.Ston)
type Config struct {
- Blocks int
- Percentile int
- Default *big.Int `toml:",omitempty"`
+ Blocks int
+ Percentile int
+ MaxHeaderHistory int
+ MaxBlockHistory 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)
+ GetBlockReceipts(ctx context.Context, hash common.Hash) types.Receipts
+ 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 api.Backend
+ backend OracleBackend
lastHead common.Hash
lastPrice *big.Int
cacheLock sync.RWMutex
fetchLock sync.Mutex
- checkBlocks, maxEmpty, maxBlocks int
- percentile int
+ checkBlocks, maxEmpty, maxBlocks int
+ percentile int
+ maxHeaderHistory, maxBlockHistory int
}
// NewOracle returns a new oracle.
-func NewOracle(backend api.Backend, params Config) *Oracle {
+func NewOracle(backend OracleBackend, params Config) *Oracle {
blocks := params.Blocks
if blocks < 1 {
blocks = 1
@@ -65,12 +78,14 @@ func NewOracle(backend api.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,
+ lastPrice: params.Default,
+ checkBlocks: blocks,
+ maxEmpty: blocks / 2,
+ maxBlocks: blocks * 5,
+ percentile: percent,
+ maxHeaderHistory: params.MaxHeaderHistory,
+ maxBlockHistory: params.MaxBlockHistory,
}
}
diff --git a/node/cn/gasprice/gasprice_test.go b/node/cn/gasprice/gasprice_test.go
index c1a199f9cf..34939511f1 100644
--- a/node/cn/gasprice/gasprice_test.go
+++ b/node/cn/gasprice/gasprice_test.go
@@ -17,14 +17,92 @@
package gasprice
import (
+ "context"
"math/big"
"testing"
+ "github.com/klaytn/klaytn/blockchain"
+ "github.com/klaytn/klaytn/blockchain/types"
+ "github.com/klaytn/klaytn/blockchain/vm"
+ "github.com/klaytn/klaytn/common"
+ "github.com/klaytn/klaytn/common/math"
+ "github.com/klaytn/klaytn/consensus/gxhash"
+ "github.com/klaytn/klaytn/crypto"
+ "github.com/klaytn/klaytn/networks/rpc"
+ "github.com/klaytn/klaytn/params"
+ "github.com/klaytn/klaytn/storage/database"
+
"github.com/golang/mock/gomock"
mock_api "github.com/klaytn/klaytn/api/mocks"
"github.com/stretchr/testify/assert"
)
+const testHead = 32
+
+type testBackend struct {
+ chain *blockchain.BlockChain
+}
+
+func (b *testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
+ if number > testHead {
+ return nil, nil
+ }
+ if number == rpc.LatestBlockNumber {
+ number = testHead
+ }
+ return b.chain.GetHeaderByNumber(uint64(number)), nil
+}
+
+func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
+ if number > testHead {
+ return nil, nil
+ }
+ if number == rpc.LatestBlockNumber {
+ number = testHead
+ }
+ return b.chain.GetBlockByNumber(uint64(number)), nil
+}
+
+func (b *testBackend) GetBlockReceipts(ctx context.Context, hash common.Hash) types.Receipts {
+ return b.chain.GetReceiptsByBlockHash(hash)
+}
+
+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 = &blockchain.Genesis{
+ Config: params.TestChainConfig,
+ Alloc: blockchain.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}},
+ }
+ db = database.NewMemoryDBManager()
+ genesis = gspec.MustCommit(db)
+ )
+ // Generate testing blocks
+ blocks, _ := blockchain.GenerateChain(gspec.Config, genesis, gxhash.NewFaker(), db, testHead+1, func(i int, b *blockchain.BlockGen) {
+ b.SetRewardbase(addr)
+
+ toaddr := common.Address{}
+ data := make([]byte, 1)
+ gas, _ := types.IntrinsicGas(data, nil, false, params.TestChainConfig.Rules(big.NewInt(0)))
+ signer := types.NewEIP155Signer(params.TestChainConfig.ChainID)
+ tx, _ := types.SignTx(types.NewTransaction(b.TxNonce(addr), toaddr, big.NewInt(1), gas, nil, data), signer, key)
+ b.AddTx(tx)
+ })
+ // Construct testing chain
+ chain, err := blockchain.NewBlockChain(db, nil, gspec.Config, gxhash.NewFaker(), vm.Config{})
+ if err != nil {
+ t.Fatalf("Failed to create local chain, %v", err)
+ }
+ chain.InsertChain(blocks)
+ return &testBackend{chain: chain}
+}
+
func TestGasPrice_NewOracle(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()