mirror of
https://github.com/status-im/op-geth.git
synced 2025-02-12 06:46:25 +00:00
implement more accurate & predicable priority fee suggestion algorithm specific for chains like Optimism
This commit is contained in:
parent
3fa9e81244
commit
99ffcfa4fa
@ -168,6 +168,11 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) {
|
||||
if headHash == lastHead {
|
||||
return new(big.Int).Set(lastPrice), nil
|
||||
}
|
||||
|
||||
if oracle.backend.ChainConfig().IsOptimism() {
|
||||
return oracle.SuggestOptimismPriorityFee(ctx, head, headHash), nil
|
||||
}
|
||||
|
||||
var (
|
||||
sent, exp int
|
||||
number = head.Number.Uint64()
|
||||
|
116
eth/gasprice/optimism-gasprice.go
Normal file
116
eth/gasprice/optimism-gasprice.go
Normal file
@ -0,0 +1,116 @@
|
||||
package gasprice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: What's the recommended way to make this a configurable parameter?
|
||||
MinSuggestedOptimismPriorityFee = big.NewInt(1e8 * params.Wei) // 0.1 gwei
|
||||
)
|
||||
|
||||
// SuggestOptimismPriorityFee returns a max priority fee value that can be used such that newly
|
||||
// created transactions have a very high chance to be included in the following blocks, using a
|
||||
// simplified and more predictable algorithm appropriate for chains like Optimism with a single
|
||||
// known block builder.
|
||||
//
|
||||
// In the typical case, which results whenever the last block had room for more transactions, this
|
||||
// function returns a minimum suggested priority fee value. Otherwise it returns the higher of this
|
||||
// minimum suggestion or 10% over the median effective priority fee from the last block.
|
||||
//
|
||||
// Rationale: For a chain such as Optimism where there is a single block builder whose behavior is
|
||||
// known, we know priority fee (as long as it is non-zero) has no impact on the probability for tx
|
||||
// inclusion as long as there is capacity for it in the block. In this case then, there's no reason
|
||||
// to return any value higher than some fixed minimum. Blocks typically reach capacity only under
|
||||
// extreme events such as airdrops, meaning predicting whether the next block is going to be at
|
||||
// capacity is difficult *except* in the case where we're already experiencing the increased demand
|
||||
// from such an event. We therefore expect whether the last known block is at capacity to be one of
|
||||
// the best predictors of whether the next block is likely to be at capacity. (An even better
|
||||
// predictor is to look at the state of the transaction pool, but we want an algorithm that works
|
||||
// even if the txpool is private or unavailable.)
|
||||
//
|
||||
// In the event the next block may be at capacity, the algorithm should allow for average fees to
|
||||
// rise in order to reach a market price that appropriately reflects demand. We accomplish this by
|
||||
// returning a suggestion that is a significant amount (10%) higher than the median effective
|
||||
// priority fee from the previous block.
|
||||
func (oracle *Oracle) SuggestOptimismPriorityFee(ctx context.Context, h *types.Header, headHash common.Hash) *big.Int {
|
||||
suggestion := new(big.Int).Set(MinSuggestedOptimismPriorityFee)
|
||||
|
||||
// find the maximum gas used by any of the transactions in the block to use as the capacity
|
||||
// margin
|
||||
receipts, err := oracle.backend.GetReceipts(ctx, headHash)
|
||||
if receipts == nil || err != nil {
|
||||
log.Error("failed to get block receipts", "err", err)
|
||||
return suggestion
|
||||
}
|
||||
var maxTxGasUsed uint64
|
||||
for i := range receipts {
|
||||
gu := receipts[i].GasUsed
|
||||
if gu > maxTxGasUsed {
|
||||
maxTxGasUsed = gu
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check the max gas used value
|
||||
if maxTxGasUsed > h.GasLimit {
|
||||
log.Error("found tx consuming more gas than the block limit", "gas", maxTxGasUsed)
|
||||
return suggestion
|
||||
}
|
||||
|
||||
if h.GasUsed+maxTxGasUsed > h.GasLimit {
|
||||
// A block is "at capacity" if, when it is built, there is a pending tx in the txpool that
|
||||
// could not be included because the block's gas limit would be exceeded. Since we don't
|
||||
// have access to the txpool, we instead adopt the following heuristic: consider a block as
|
||||
// at capacity if the total gas consumed by its transactions is within max-tx-gas-used of
|
||||
// the block limit, where max-tx-gas-used is the most gas used by any one transaction
|
||||
// within the block. This heuristic is almost perfectly accurate when transactions always
|
||||
// consume the same amount of gas, but becomes less accurate as tx gas consumption begins
|
||||
// to vary. The typical error is we assume a block is at capacity when it was not because
|
||||
// max-tx-gas-used will in most cases over-estimate the "capacity margin". But it's better
|
||||
// to err on the side of returning a higher-than-needed suggestion than a lower-than-needed
|
||||
// one in order to satisfy our desire for high chance of inclusion and rising fees under
|
||||
// high demand.
|
||||
block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(h.Number.Int64()))
|
||||
if block == nil || err != nil {
|
||||
log.Error("failed to get last block", "err", err)
|
||||
return suggestion
|
||||
}
|
||||
baseFee := block.BaseFee()
|
||||
txs := block.Transactions()
|
||||
if len(txs) == 0 {
|
||||
log.Error("block was at capacity but doesn't have transactions")
|
||||
return suggestion
|
||||
}
|
||||
tips := bigIntArray(make([]*big.Int, len(txs)))
|
||||
for i := range txs {
|
||||
tips[i] = txs[i].EffectiveGasTipValue(baseFee)
|
||||
}
|
||||
sort.Sort(tips)
|
||||
median := tips[len(tips)/2]
|
||||
newSuggestion := new(big.Int).Add(median, new(big.Int).Div(median, big.NewInt(10)))
|
||||
// use the new suggestion only if it's bigger than the minimum
|
||||
if newSuggestion.Cmp(suggestion) > 0 {
|
||||
suggestion = newSuggestion
|
||||
}
|
||||
}
|
||||
|
||||
// the suggestion should be capped by oracle.maxPrice
|
||||
if suggestion.Cmp(oracle.maxPrice) > 0 {
|
||||
suggestion.Set(oracle.maxPrice)
|
||||
}
|
||||
|
||||
oracle.cacheLock.Lock()
|
||||
oracle.lastHead = headHash
|
||||
oracle.lastPrice = suggestion
|
||||
oracle.cacheLock.Unlock()
|
||||
|
||||
return new(big.Int).Set(suggestion)
|
||||
}
|
141
eth/gasprice/optimism-gasprice_test.go
Normal file
141
eth/gasprice/optimism-gasprice_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gasprice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
const (
|
||||
blockGasLimit = params.TxGas * 3
|
||||
)
|
||||
|
||||
type testTxData struct {
|
||||
priorityFee int64
|
||||
gasLimit uint64
|
||||
}
|
||||
|
||||
type opTestBackend struct {
|
||||
block *types.Block
|
||||
receipts []*types.Receipt
|
||||
}
|
||||
|
||||
func (b *opTestBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (b *opTestBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
|
||||
return b.block, nil
|
||||
}
|
||||
|
||||
func (b *opTestBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
|
||||
return b.receipts, nil
|
||||
}
|
||||
|
||||
func (b *opTestBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (b *opTestBackend) ChainConfig() *params.ChainConfig {
|
||||
return params.TestChainConfig
|
||||
}
|
||||
|
||||
func (b *opTestBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newOpTestBackend(t *testing.T, txs []testTxData) *opTestBackend {
|
||||
var (
|
||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
signer = types.LatestSigner(params.TestChainConfig)
|
||||
)
|
||||
// for optimism price suggestion only the most recent block is considered so this is where
|
||||
// we add the test transactions
|
||||
ts := []*types.Transaction{}
|
||||
rs := []*types.Receipt{}
|
||||
header := types.Header{}
|
||||
header.GasLimit = blockGasLimit
|
||||
var nonce uint64
|
||||
for _, tx := range txs {
|
||||
txdata := &types.DynamicFeeTx{
|
||||
ChainID: params.TestChainConfig.ChainID,
|
||||
Nonce: nonce,
|
||||
To: &common.Address{},
|
||||
Gas: params.TxGas,
|
||||
GasFeeCap: big.NewInt(100 * params.GWei),
|
||||
GasTipCap: big.NewInt(tx.priorityFee),
|
||||
Data: []byte{},
|
||||
}
|
||||
t := types.MustSignNewTx(key, signer, txdata)
|
||||
ts = append(ts, t)
|
||||
r := types.Receipt{}
|
||||
r.GasUsed = tx.gasLimit
|
||||
header.GasUsed += r.GasUsed
|
||||
rs = append(rs, &r)
|
||||
nonce++
|
||||
}
|
||||
hasher := trie.NewStackTrie(nil)
|
||||
b := types.NewBlock(&header, ts, nil, nil, hasher)
|
||||
return &opTestBackend{block: b, receipts: rs}
|
||||
}
|
||||
|
||||
func TestSuggestOptimismPriorityFee(t *testing.T) {
|
||||
var cases = []struct {
|
||||
txdata []testTxData
|
||||
want *big.Int
|
||||
}{
|
||||
{
|
||||
// block well under capacity, expect min priority fee suggestion
|
||||
txdata: []testTxData{testTxData{params.GWei, 21000}},
|
||||
want: MinSuggestedOptimismPriorityFee,
|
||||
},
|
||||
{
|
||||
// 2 txs, still under capacity, expect min priority fee suggestion
|
||||
txdata: []testTxData{testTxData{params.GWei, 21000}, testTxData{params.GWei, 21000}},
|
||||
want: MinSuggestedOptimismPriorityFee,
|
||||
},
|
||||
{
|
||||
// 2 txs w same priority fee (1 gwei), but second tx puts it right over capacity
|
||||
txdata: []testTxData{testTxData{params.GWei, 21000}, testTxData{params.GWei, 21001}},
|
||||
want: big.NewInt(1100000000), // 10 percent over 1 gwei, the median
|
||||
},
|
||||
{
|
||||
// 3 txs, full block. return 10% over the median tx (10 gwei * 10% == 11 gwei)
|
||||
txdata: []testTxData{testTxData{10 * params.GWei, 21000}, testTxData{1 * params.GWei, 21000}, testTxData{100 * params.GWei, 21000}},
|
||||
want: big.NewInt(11 * params.GWei),
|
||||
},
|
||||
}
|
||||
for i, c := range cases {
|
||||
backend := newOpTestBackend(t, c.txdata)
|
||||
oracle := NewOracle(backend, Config{})
|
||||
got := oracle.SuggestOptimismPriorityFee(context.Background(), backend.block.Header(), backend.block.Hash())
|
||||
if got.Cmp(c.want) != 0 {
|
||||
t.Errorf("Gas price mismatch for test case %d: want %d, got %d", i, c.want, got)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user