gas oracle work

This commit is contained in:
Ian Norden 2020-01-10 15:30:04 -06:00
parent 6575396460
commit 4d414e40c8
12 changed files with 404 additions and 33 deletions

View File

@ -154,6 +154,20 @@ func (b *SimulatedBackend) CodeAt(ctx context.Context, contract common.Address,
return statedb.GetCode(contract), nil
}
// BaseFeeAt returns the BaseFee at the given block height.
// If the blockNumber is nil the latest known BaseFee is returned.
func (b *SimulatedBackend) BaseFeeAt(ctx context.Context, blockNumber *big.Int) (*big.Int, error) {
if blockNumber == nil || blockNumber.Cmp(b.pendingBlock.Number()) == 0 {
header := b.blockchain.CurrentHeader()
return header.BaseFee, nil
}
header, err := b.HeaderByNumber(ctx, blockNumber)
if err != nil {
return nil, err
}
return header.BaseFee, nil
}
// BalanceAt returns the wei balance of a certain account in the blockchain.
func (b *SimulatedBackend) BalanceAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (*big.Int, error) {
b.mu.Lock()
@ -430,6 +444,16 @@ func (b *SimulatedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error
return big.NewInt(1), nil
}
// SuggestGasPremium, since the simulated chain doesn't have miners, we just return a GasPremium of 1 for any call
func (b *SimulatedBackend) SuggestGasPremium(ctx context.Context) (*big.Int, error) {
return big.NewInt(1), nil
}
// SuggestFeeCap, since the simulated chain doesn't have miners, we just return a FeeCap of 1 for any call
func (b *SimulatedBackend) SuggestFeeCap(ctx context.Context) (*big.Int, error) {
return big.NewInt(1), nil
}
// EstimateGas executes the requested code against the currently pending block/state and
// returns the used amount of gas.
func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) {

View File

@ -49,10 +49,14 @@ type TransactOpts struct {
Nonce *big.Int // Nonce to use for the transaction execution (nil = use pending state)
Signer SignerFn // Method to use for signing the transaction (mandatory)
Value *big.Int // Funds to transfer along the transaction (nil = 0 = no funds)
GasPrice *big.Int // Gas price to use for the transaction execution (nil = gas price oracle)
Value *big.Int // Funds to transfer along along the transaction (nil = 0 = no funds)
GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate)
// If GasPrice, GasPremium, and FeeCap are all nil then we defer to the gas price oracle
GasPrice *big.Int // Gas price to use for the transaction execution
GasPremium *big.Int // Gas premium (tip) to use for EIP1559 transaction execution (
FeeCap *big.Int // Fee cap to use for EIP1559 transaction execution
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
}
@ -213,7 +217,7 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
}
// Figure out the gas allowance and gas price values
gasPrice := opts.GasPrice
if gasPrice == nil {
if gasPrice == nil && opts.FeeCap == nil && opts.GasPremium == nil {
gasPrice, err = c.transactor.SuggestGasPrice(ensureContext(opts.Context))
if err != nil {
return nil, fmt.Errorf("failed to suggest gas price: %v", err)
@ -230,7 +234,15 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
}
}
// If the contract surely has code (or code is not needed), estimate the transaction
msg := ethereum.CallMsg{From: opts.From, To: contract, GasPrice: gasPrice, Value: value, Data: input}
msg := ethereum.CallMsg{
From: opts.From,
To: contract,
GasPrice: gasPrice,
Value: value,
Data: input,
GasPremium: opts.GasPremium,
FeeCap: opts.FeeCap,
}
gasLimit, err = c.transactor.EstimateGas(ensureContext(opts.Context), msg)
if err != nil {
return nil, fmt.Errorf("failed to estimate gas needed: %v", err)
@ -239,9 +251,9 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
// Create the transaction, sign it and schedule it for execution
var rawTx *types.Transaction
if contract == nil {
rawTx = types.NewContractCreation(nonce, value, gasLimit, gasPrice, input, nil, nil)
rawTx = types.NewContractCreation(nonce, value, gasLimit, gasPrice, input, opts.GasPremium, opts.FeeCap)
} else {
rawTx = types.NewTransaction(nonce, c.address, value, gasLimit, gasPrice, input, nil, nil)
rawTx = types.NewTransaction(nonce, c.address, value, gasLimit, gasPrice, input, opts.GasPremium, opts.FeeCap)
}
if opts.Signer == nil {
return nil, errors.New("no signer to authorize the transaction with")

View File

@ -200,13 +200,15 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
to = &t
}
args := &core.SendTxArgs{
Data: &data,
Nonce: hexutil.Uint64(tx.Nonce()),
Value: hexutil.Big(*tx.Value()),
Gas: hexutil.Uint64(tx.Gas()),
GasPrice: hexutil.Big(*tx.GasPrice()),
To: to,
From: common.NewMixedcaseAddress(account.Address),
Data: &data,
Nonce: hexutil.Uint64(tx.Nonce()),
Value: hexutil.Big(*tx.Value()),
Gas: hexutil.Uint64(tx.Gas()),
GasPrice: (*hexutil.Big)(tx.GasPrice()),
GasPremium: (*hexutil.Big)(tx.GasPremium()),
FeeCap: (*hexutil.Big)(tx.FeeCap()),
To: to,
From: common.NewMixedcaseAddress(account.Address),
}
if err := api.client.Call(&res, "account_signTransaction", args); err != nil {
return nil, err

View File

@ -883,7 +883,7 @@ func testExternalUI(api *core.SignerAPI) {
Value: hexutil.Big(*big.NewInt(6)),
From: common.NewMixedcaseAddress(a),
To: &to,
GasPrice: hexutil.Big(*big.NewInt(5)),
GasPrice: (*hexutil.Big)(big.NewInt(5)),
Gas: 1000,
Input: nil,
}
@ -1040,7 +1040,7 @@ func GenDoc(ctx *cli.Context) {
Value: hexutil.Big(*big.NewInt(6)),
From: common.NewMixedcaseAddress(a),
To: nil,
GasPrice: hexutil.Big(*big.NewInt(5)),
GasPrice: (*hexutil.Big)(big.NewInt(5)),
Gas: 1000,
Input: nil,
}})
@ -1056,7 +1056,7 @@ func GenDoc(ctx *cli.Context) {
Value: hexutil.Big(*big.NewInt(6)),
From: common.NewMixedcaseAddress(a),
To: nil,
GasPrice: hexutil.Big(*big.NewInt(5)),
GasPrice: (*hexutil.Big)(big.NewInt(5)),
Gas: 1000,
Input: nil,
}})

View File

@ -273,6 +273,14 @@ func (b *EthAPIBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPrice(ctx)
}
func (b *EthAPIBackend) SuggestPremium(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPremium(ctx)
}
func (b *EthAPIBackend) SuggestCap(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestCap(ctx)
}
func (b *EthAPIBackend) ChainDb() ethdb.Database {
return b.eth.ChainDb()
}

View File

@ -221,8 +221,22 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
eth.APIBackend = &EthAPIBackend{ctx.ExtRPCEnabled(), eth, nil}
gpoParams := config.GPO
if gpoParams.Default == nil {
gpoParams.Default = config.Miner.GasPrice
if gpoParams.DefaultGasPrice == nil {
gpoParams.DefaultGasPrice = config.Miner.GasPrice
}
if gpoParams.DefaultFeeCap == nil {
gpoParams.DefaultFeeCap = config.Miner.GasPrice
}
if gpoParams.DefaultGasPremium == nil {
baseFee := eth.blockchain.CurrentHeader().BaseFee
if baseFee == nil {
baseFee = new(big.Int).SetUint64(params.EIP1559InitialBaseFee)
}
gasPremium := new(big.Int).Sub(config.Miner.GasPrice, baseFee)
if gasPremium.Cmp(big.NewInt(0)) < 0 {
gasPremium = big.NewInt(0)
}
gpoParams.DefaultGasPremium = gasPremium
}
eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams)

View File

@ -29,22 +29,31 @@ import (
"github.com/ethereum/go-ethereum/rpc"
)
var maxPrice = big.NewInt(500 * params.GWei)
var (
maxPrice = big.NewInt(500 * params.GWei)
maxPremium = big.NewInt(500 * params.GWei)
maxFeeCap = big.NewInt(1000 * params.GWei)
)
type Config struct {
Blocks int
Percentile int
Default *big.Int `toml:",omitempty"`
Blocks int
Percentile int
DefaultGasPrice *big.Int `toml:",omitempty"`
DefaultGasPremium *big.Int `toml:",omitempty"`
DefaultFeeCap *big.Int `toml:",omitempty"`
}
// 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
backend ethapi.Backend
lastHead common.Hash
lastPrice *big.Int
lastPremium *big.Int
lastCap *big.Int
lastBaseFee *big.Int
cacheLock sync.RWMutex
fetchLock sync.Mutex
checkBlocks, maxEmpty, maxBlocks int
percentile int
@ -65,7 +74,9 @@ func NewOracle(backend ethapi.Backend, params Config) *Oracle {
}
return &Oracle{
backend: backend,
lastPrice: params.Default,
lastPrice: params.DefaultGasPrice,
lastPremium: params.DefaultGasPremium,
lastCap: params.DefaultFeeCap,
checkBlocks: blocks,
maxEmpty: blocks / 2,
maxBlocks: blocks * 5,
@ -147,16 +158,216 @@ func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
return price, nil
}
// SuggestPremium returns the recommended gas premium.
func (gpo *Oracle) SuggestPremium(ctx context.Context) (*big.Int, error) {
gpo.cacheLock.RLock()
lastHead := gpo.lastHead
lastPremium := gpo.lastPremium
gpo.cacheLock.RUnlock()
head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash()
if headHash == lastHead {
return lastPremium, 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
lastPremium = gpo.lastPremium
gpo.cacheLock.RUnlock()
if headHash == lastHead {
return lastPremium, nil
}
blockNum := head.Number.Uint64()
ch := make(chan getBlockPremiumsResult, gpo.checkBlocks)
sent := 0
exp := 0
var blockPremiums []*big.Int
for sent < gpo.checkBlocks && blockNum > 0 {
go gpo.getBlockPremiums(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
sent++
exp++
blockNum--
}
maxEmpty := gpo.maxEmpty
for exp > 0 {
res := <-ch
if res.err != nil {
return lastPremium, res.err
}
exp--
if res.premium != nil {
blockPremiums = append(blockPremiums, res.premium)
continue
}
if maxEmpty > 0 {
maxEmpty--
continue
}
if blockNum > 0 && sent < gpo.maxBlocks {
go gpo.getBlockPremiums(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
sent++
exp++
blockNum--
}
}
premium := lastPremium
if len(blockPremiums) > 0 {
sort.Sort(bigIntArray(blockPremiums))
premium = blockPremiums[(len(blockPremiums)-1)*gpo.percentile/100]
}
if premium.Cmp(maxPremium) > 0 {
premium = new(big.Int).Set(maxPremium)
}
gpo.cacheLock.Lock()
gpo.lastHead = headHash
gpo.lastPremium = premium
gpo.cacheLock.Unlock()
return premium, nil
}
// SuggestCap returns the recommended fee cap.
func (gpo *Oracle) SuggestCap(ctx context.Context) (*big.Int, error) {
gpo.cacheLock.RLock()
lastHead := gpo.lastHead
lastCap := gpo.lastCap
gpo.cacheLock.RUnlock()
head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash()
if headHash == lastHead {
return lastCap, 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
lastCap = gpo.lastCap
gpo.cacheLock.RUnlock()
if headHash == lastHead {
return lastCap, nil
}
blockNum := head.Number.Uint64()
ch := make(chan getBlockCapsResult, gpo.checkBlocks)
sent := 0
exp := 0
var blockCaps []*big.Int
for sent < gpo.checkBlocks && blockNum > 0 {
go gpo.getBlockCaps(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
sent++
exp++
blockNum--
}
maxEmpty := gpo.maxEmpty
for exp > 0 {
res := <-ch
if res.err != nil {
return lastCap, res.err
}
exp--
if res.cap != nil {
blockCaps = append(blockCaps, res.cap)
continue
}
if maxEmpty > 0 {
maxEmpty--
continue
}
if blockNum > 0 && sent < gpo.maxBlocks {
go gpo.getBlockCaps(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(blockNum))), blockNum, ch)
sent++
exp++
blockNum--
}
}
cap := lastCap
if len(blockCaps) > 0 {
sort.Sort(bigIntArray(blockCaps))
cap = blockCaps[(len(blockCaps)-1)*gpo.percentile/100]
}
if cap.Cmp(maxFeeCap) > 0 {
cap = new(big.Int).Set(maxFeeCap)
}
gpo.cacheLock.Lock()
gpo.lastHead = headHash
gpo.lastCap = cap
gpo.cacheLock.Unlock()
return cap, nil
}
type getBlockPricesResult struct {
price *big.Int
err error
}
type getBlockPremiumsResult struct {
premium *big.Int
err error
}
type getBlockCapsResult struct {
cap *big.Int
err error
}
type transactionsByGasPrice []*types.Transaction
func (t transactionsByGasPrice) Len() int { return len(t) }
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].GasPrice().Cmp(t[j].GasPrice()) < 0 }
func (t transactionsByGasPrice) Len() int { return len(t) }
func (t transactionsByGasPrice) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t transactionsByGasPrice) Less(i, j int) bool {
iPrice := t[i].GasPrice()
jPrice := t[j].GasPrice()
if iPrice == nil {
iPrice = big.NewInt(0)
}
if jPrice == nil {
jPrice = big.NewInt(0)
}
return iPrice.Cmp(jPrice) < 0
}
type transactionsByGasPremium []*types.Transaction
func (t transactionsByGasPremium) Len() int { return len(t) }
func (t transactionsByGasPremium) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t transactionsByGasPremium) Less(i, j int) bool {
iPremium := t[i].GasPremium()
jPremium := t[j].GasPremium()
if iPremium == nil {
iPremium = big.NewInt(0)
}
if jPremium == nil {
jPremium = big.NewInt(0)
}
return iPremium.Cmp(jPremium) < 0
}
type transactionsByFeeCap []*types.Transaction
func (t transactionsByFeeCap) Len() int { return len(t) }
func (t transactionsByFeeCap) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t transactionsByFeeCap) Less(i, j int) bool {
iCap := t[i].FeeCap()
jCap := t[j].FeeCap()
if iCap == nil {
iCap = big.NewInt(0)
}
if jCap == nil {
jCap = big.NewInt(0)
}
return iCap.Cmp(jCap) < 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.
@ -182,6 +393,54 @@ func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, bloc
ch <- getBlockPricesResult{nil, nil}
}
// getBlockPremiums calculates the lowest transaction gas premium in a given block
// and sends it to the result channel. If the block is empty, price is nil.
func (gpo *Oracle) getBlockPremiums(ctx context.Context, signer types.Signer, blockNum uint64, ch chan getBlockPremiumsResult) {
block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
ch <- getBlockPremiumsResult{nil, err}
return
}
blockTxs := block.Transactions()
txs := make([]*types.Transaction, len(blockTxs))
copy(txs, blockTxs)
sort.Sort(transactionsByGasPremium(txs))
for _, tx := range txs {
sender, err := types.Sender(signer, tx)
if err == nil && sender != block.Coinbase() {
ch <- getBlockPremiumsResult{tx.GasPremium(), nil}
return
}
}
ch <- getBlockPremiumsResult{nil, nil}
}
// getBlockCaps calculates the lowest transaction fee cap in a given block
// and sends it to the result channel. If the block is empty, price is nil.
func (gpo *Oracle) getBlockCaps(ctx context.Context, signer types.Signer, blockNum uint64, ch chan getBlockCapsResult) {
block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
ch <- getBlockCapsResult{nil, err}
return
}
blockTxs := block.Transactions()
txs := make([]*types.Transaction, len(blockTxs))
copy(txs, blockTxs)
sort.Sort(transactionsByFeeCap(txs))
for _, tx := range txs {
sender, err := types.Sender(signer, tx)
if err == nil && sender != block.Coinbase() {
ch <- getBlockCapsResult{tx.FeeCap(), nil}
return
}
}
ch <- getBlockCapsResult{nil, nil}
}
type bigIntArray []*big.Int
func (s bigIntArray) Len() int { return len(s) }

View File

@ -371,6 +371,16 @@ func (ec *Client) NonceAt(ctx context.Context, account common.Address, blockNumb
return uint64(result), err
}
// BaseFeeAt returns the BaseFee at the given block height.
// If the blockNumber is nil the latest known BaseFee is returned.
func (ec *Client) BaseFeeAt(ctx context.Context, blockNumber *big.Int) (*big.Int, error) {
header, err := ec.HeaderByNumber(ctx, blockNumber)
if err != nil {
return nil, err
}
return header.BaseFee, nil
}
// Filters
// FilterLogs executes a filter query.
@ -492,6 +502,26 @@ func (ec *Client) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
return (*big.Int)(&hex), nil
}
// SuggestGasPremium retrieves the currently suggested gas premium to allow a timely
// execution of a transaction
func (ec *Client) SuggestGasPremium(ctx context.Context) (*big.Int, error) {
var hex hexutil.Big
if err := ec.c.CallContext(ctx, &hex, "eth_gasPremium"); err != nil {
return nil, err
}
return (*big.Int)(&hex), nil
}
// SuggestFeeCap retrieves the currently suggested fee cap to allow a timely
// execution of a transaction
func (ec *Client) SuggestFeeCap(ctx context.Context) (*big.Int, error) {
var hex hexutil.Big
if err := ec.c.CallContext(ctx, &hex, "eth_feeCap"); err != nil {
return nil, err
}
return (*big.Int)(&hex), nil
}
// EstimateGas tries to estimate the gas needed to execute a specific transaction based on
// the current pending state of the backend blockchain. There is no guarantee that this is
// the true gas limit requirement as other transactions may be added or removed by miners,

View File

@ -65,6 +65,18 @@ func (s *PublicEthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error)
return (*hexutil.Big)(price), err
}
// GasPremium returns a suggestion for the gas premium.
func (s *PublicEthereumAPI) GasPremium(ctx context.Context) (*hexutil.Big, error) {
premium, err := s.b.SuggestPremium(ctx)
return (*hexutil.Big)(premium), err
}
// FeeCap returns a suggestion for the fee cap.
func (s *PublicEthereumAPI) FeeCap(ctx context.Context) (*hexutil.Big, error) {
cap, err := s.b.SuggestCap(ctx)
return (*hexutil.Big)(cap), err
}
// ProtocolVersion returns the current Ethereum protocol version this node supports
func (s *PublicEthereumAPI) ProtocolVersion() hexutil.Uint {
return hexutil.Uint(s.b.ProtocolVersion())

View File

@ -42,6 +42,8 @@ type Backend interface {
Downloader() *downloader.Downloader
ProtocolVersion() int
SuggestPrice(ctx context.Context) (*big.Int, error)
SuggestPremium(ctx context.Context) (*big.Int, error)
SuggestCap(ctx context.Context) (*big.Int, error)
ChainDb() ethdb.Database
AccountManager() *accounts.Manager
ExtRPCEnabled() bool

View File

@ -246,6 +246,14 @@ func (b *LesApiBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPrice(ctx)
}
func (b *LesApiBackend) SuggestPremium(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPremium(ctx)
}
func (b *LesApiBackend) SuggestCap(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestCap(ctx)
}
func (b *LesApiBackend) ChainDb() ethdb.Database {
return b.eth.chainDb
}

View File

@ -158,8 +158,8 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
leth.ApiBackend = &LesApiBackend{ctx.ExtRPCEnabled(), leth, nil}
gpoParams := config.GPO
if gpoParams.Default == nil {
gpoParams.Default = config.Miner.GasPrice
if gpoParams.DefaultGasPrice == nil {
gpoParams.DefaultGasPrice = config.Miner.GasPrice
}
leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams)