Skip to content

Commit

Permalink
Merge pull request #314 from uprendis/feature/reactive-gas-oracle
Browse files Browse the repository at this point in the history
Reactive gas price oracle
  • Loading branch information
uprendis authored May 24, 2022
2 parents 42bb963 + 54a672f commit f591585
Show file tree
Hide file tree
Showing 17 changed files with 726 additions and 246 deletions.
60 changes: 9 additions & 51 deletions ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import (
"github.com/Fantom-foundation/go-opera/evmcore"
"github.com/Fantom-foundation/go-opera/gossip/gasprice"
"github.com/Fantom-foundation/go-opera/opera"
"github.com/Fantom-foundation/go-opera/utils/piecefunc"
"github.com/Fantom-foundation/go-opera/utils/signers/gsignercache"
"github.com/Fantom-foundation/go-opera/utils/signers/internaltx"
)
Expand All @@ -72,21 +71,15 @@ func NewPublicEthereumAPI(b Backend) *PublicEthereumAPI {

// GasPrice returns a suggestion for a gas price for legacy transactions.
func (s *PublicEthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) {
tipcap, err := s.b.SuggestGasTipCap(ctx)
if err != nil {
return nil, err
}
tipcap := s.b.SuggestGasTipCap(ctx, gasprice.AsDefaultCertainty)
tipcap.Add(tipcap, s.b.MinGasPrice())
return (*hexutil.Big)(tipcap), nil
}

// MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee transactions.
func (s *PublicEthereumAPI) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, error) {
tipcap, err := s.b.SuggestGasTipCap(ctx)
if err != nil {
return nil, err
}
return (*hexutil.Big)(tipcap), err
tipcap := s.b.SuggestGasTipCap(ctx, gasprice.AsDefaultCertainty)
return (*hexutil.Big)(tipcap), nil
}

type feeHistoryResult struct {
Expand All @@ -96,40 +89,6 @@ type feeHistoryResult struct {
GasUsedRatio []float64 `json:"gasUsedRatio"`
}

func scaleGasTip(tip, baseFee *big.Int, ratio uint64) *big.Int {
// max((SuggestedGasTip+minGasPrice)*0.6-minGasPrice, 0)
min := baseFee
est := new(big.Int).Set(tip)
est.Add(est, min)
est.Mul(est, new(big.Int).SetUint64(ratio))
est.Div(est, gasprice.DecimalUnitBn)
est.Sub(est, min)
if est.Sign() < 0 {
return new(big.Int)
}

return est
}

var tipScaleRatio = piecefunc.NewFunc([]piecefunc.Dot{
{
X: 0,
Y: 0.7 * gasprice.DecimalUnit,
},
{
X: 0.2 * gasprice.DecimalUnit,
Y: 1.0 * gasprice.DecimalUnit,
},
{
X: 0.8 * gasprice.DecimalUnit,
Y: 1.2 * gasprice.DecimalUnit,
},
{
X: 1.0 * gasprice.DecimalUnit,
Y: 2.0 * gasprice.DecimalUnit,
},
})

var errInvalidPercentile = errors.New("invalid reward percentile")

func (s *PublicEthereumAPI) FeeHistory(ctx context.Context, blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*feeHistoryResult, error) {
Expand Down Expand Up @@ -166,16 +125,11 @@ func (s *PublicEthereumAPI) FeeHistory(ctx context.Context, blockCount rpc.Decim
}

baseFee := s.b.MinGasPrice()
goldTip, err := s.b.SuggestGasTipCap(ctx)
if err != nil {
return nil, err
}

tips := make([]*hexutil.Big, 0, len(rewardPercentiles))
for _, p := range rewardPercentiles {
ratio := tipScaleRatio(uint64(gasprice.DecimalUnit * p / 100.0))
scaledTip := scaleGasTip(goldTip, baseFee, ratio)
tips = append(tips, (*hexutil.Big)(scaledTip))
tip := s.b.SuggestGasTipCap(ctx, uint64(gasprice.DecimalUnit*p/100.0))
tips = append(tips, (*hexutil.Big)(tip))
}
res.OldestBlock.ToInt().SetUint64(uint64(oldest))
for i := uint64(0); i < uint64(last-oldest+1); i++ {
Expand All @@ -186,6 +140,10 @@ func (s *PublicEthereumAPI) FeeHistory(ctx context.Context, blockCount rpc.Decim
return res, nil
}

func (s *PublicEthereumAPI) EffectiveBaseFee(ctx context.Context) *hexutil.Big {
return (*hexutil.Big)(s.b.EffectiveMinGasPrice(ctx))
}

// Syncing returns true if node is syncing
func (s *PublicEthereumAPI) Syncing() (interface{}, error) {
progress := s.b.Progress()
Expand Down
3 changes: 2 additions & 1 deletion ethapi/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ type PeerProgress struct {
type Backend interface {
// General Ethereum API
Progress() PeerProgress
SuggestGasTipCap(ctx context.Context) (*big.Int, error)
SuggestGasTipCap(ctx context.Context, certainty uint64) *big.Int
EffectiveMinGasPrice(ctx context.Context) *big.Int
ChainDb() ethdb.Database
AccountManager() *accounts.Manager
ExtRPCEnabled() bool
Expand Down
12 changes: 4 additions & 8 deletions ethapi/transaction_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"

"github.com/Fantom-foundation/go-opera/gossip/gasprice"
)

// TransactionArgs represents the arguments to construct a new transaction
Expand Down Expand Up @@ -86,10 +88,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
// In this clause, user left some fields unspecified.
if b.ChainConfig().IsLondon(head.Number) && args.GasPrice == nil {
if args.MaxPriorityFeePerGas == nil {
tip, err := b.SuggestGasTipCap(ctx)
if err != nil {
return err
}
tip := b.SuggestGasTipCap(ctx, gasprice.AsDefaultCertainty)
args.MaxPriorityFeePerGas = (*hexutil.Big)(tip)
}
if args.MaxFeePerGas == nil {
Expand All @@ -107,10 +106,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
return errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet")
}
if args.GasPrice == nil {
price, err := b.SuggestGasTipCap(ctx)
if err != nil {
return err
}
price := b.SuggestGasTipCap(ctx, gasprice.AsDefaultCertainty)
price.Add(price, b.MinGasPrice())
args.GasPrice = (*hexutil.Big)(price)
}
Expand Down
4 changes: 2 additions & 2 deletions eventcheck/gaspowercheck/gas_power_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func calcGasPower(e inter.EventI, selfParent inter.EventI, ctx *ValidationContex
}

func CalcValidatorGasPower(e inter.EventI, eTime, prevTime inter.Timestamp, prevGasPowerLeft uint64, validators *pos.Validators, config Config) uint64 {
gasPowerPerSec, maxGasPower, startup := calcValidatorGasPowerPerSec(e.Creator(), validators, config)
gasPowerPerSec, maxGasPower, startup := CalcValidatorGasPowerPerSec(e.Creator(), validators, config)

if e.SelfParent() == nil {
if prevGasPowerLeft < startup {
Expand All @@ -131,7 +131,7 @@ func CalcValidatorGasPower(e inter.EventI, eTime, prevTime inter.Timestamp, prev
return gasPower
}

func calcValidatorGasPowerPerSec(
func CalcValidatorGasPowerPerSec(
validator idx.ValidatorID,
validators *pos.Validators,
config Config,
Expand Down
15 changes: 13 additions & 2 deletions evmcore/tx_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ type StateReader interface {
GetBlock(hash common.Hash, number uint64) *EvmBlock
StateAt(root common.Hash) (*state.StateDB, error)
MinGasPrice() *big.Int
RecommendedGasTip() *big.Int
EffectiveMinTip() *big.Int
MaxGasLimit() uint64
SubscribeNewBlock(ch chan<- ChainHeadNotify) notify.Subscription
Config() *params.ChainConfig
Expand Down Expand Up @@ -555,6 +555,17 @@ func (pool *TxPool) Pending(enforceTips bool) (map[common.Address]types.Transact
return pending, nil
}

func (pool *TxPool) PendingSlice() types.Transactions {
pool.mu.Lock()
defer pool.mu.Unlock()

pending := make(types.Transactions, 0, 1000)
for _, list := range pool.pending {
pending = append(pending, list.Flatten()...)
}
return pending
}

func (pool *TxPool) SampleHashes(max int) []common.Hash {
return pool.all.SampleHashes(max)
}
Expand Down Expand Up @@ -629,7 +640,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
return ErrUnderpriced
}
// Ensure Opera-specific hard bounds
if recommendedGasTip, minPrice := pool.chain.RecommendedGasTip(), pool.chain.MinGasPrice(); recommendedGasTip != nil && minPrice != nil {
if recommendedGasTip, minPrice := pool.chain.EffectiveMinTip(), pool.chain.MinGasPrice(); recommendedGasTip != nil && minPrice != nil {
if tx.GasTipCapIntCmp(recommendedGasTip) < 0 {
return ErrUnderpriced
}
Expand Down
2 changes: 1 addition & 1 deletion evmcore/tx_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (bc *testBlockChain) CurrentBlock() *EvmBlock {
func (bc *testBlockChain) MinGasPrice() *big.Int {
return common.Big0
}
func (bc *testBlockChain) RecommendedGasTip() *big.Int {
func (bc *testBlockChain) EffectiveMinTip() *big.Int {
return nil
}
func (bc *testBlockChain) MaxGasLimit() uint64 {
Expand Down
8 changes: 3 additions & 5 deletions gossip/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,9 @@ func DefaultConfig(scale cachescale.Func) Config {
},

GPO: gasprice.Config{
MaxTipCap: gasprice.DefaultMaxTipCap,
MinTipCap: new(big.Int),
MaxTipCapMultiplierRatio: big.NewInt(25 * gasprice.DecimalUnit),
MiddleTipCapMultiplierRatio: big.NewInt(3.75 * gasprice.DecimalUnit),
GasPowerWallRatio: big.NewInt(0.05 * gasprice.DecimalUnit),
MaxGasPrice: gasprice.DefaultMaxGasPrice,
MinGasPrice: new(big.Int),
DefaultCertainty: 0.5 * gasprice.DecimalUnit,
},

RPCBlockExt: true,
Expand Down
7 changes: 7 additions & 0 deletions gossip/dummy_tx_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ func (p *dummyTxPool) Pending(enforceTips bool) (map[common.Address]types.Transa
return batches, nil
}

func (p *dummyTxPool) PendingSlice() types.Transactions {
p.lock.RLock()
defer p.lock.RUnlock()

return append(make(types.Transactions, 0, len(p.pool)), p.pool...)
}

func (p *dummyTxPool) SubscribeNewTxsNotify(ch chan<- evmcore.NewTxsNotify) notify.Subscription {
return p.txFeed.Subscribe(ch)
}
Expand Down
8 changes: 6 additions & 2 deletions gossip/ethapi_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,12 @@ func (b *EthAPIBackend) TxPoolContentFrom(addr common.Address) (types.Transactio
return b.svc.txpool.ContentFrom(addr)
}

func (b *EthAPIBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return b.svc.gpo.SuggestTipCap(), nil
func (b *EthAPIBackend) SuggestGasTipCap(ctx context.Context, certainty uint64) *big.Int {
return b.svc.gpo.SuggestTip(certainty)
}

func (b *EthAPIBackend) EffectiveMinGasPrice(ctx context.Context) *big.Int {
return b.svc.gpo.EffectiveMinGasPrice()
}

func (b *EthAPIBackend) ChainDb() ethdb.Database {
Expand Down
15 changes: 3 additions & 12 deletions gossip/evm_state_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ import (
"github.com/Fantom-foundation/go-opera/opera"
)

var (
big3 = big.NewInt(3)
big5 = big.NewInt(5)
)

type EvmStateReader struct {
*ServiceFeed

Expand All @@ -46,14 +41,10 @@ func (r *EvmStateReader) MinGasPrice() *big.Int {
return r.store.GetRules().Economy.MinGasPrice
}

// RecommendedGasTip returns current soft lower bound for gas tip
func (r *EvmStateReader) RecommendedGasTip() *big.Int {
// max((SuggestedGasTip+minGasPrice)*0.6-minGasPrice, 0)
// EffectiveMinTip returns current soft lower bound for gas tip
func (r *EvmStateReader) EffectiveMinTip() *big.Int {
min := r.MinGasPrice()
est := new(big.Int).Set(r.gpo.SuggestTipCap())
est.Add(est, min)
est.Mul(est, big3)
est.Div(est, big5)
est := r.gpo.EffectiveMinGasPrice()
est.Sub(est, min)
if est.Sign() < 0 {
return new(big.Int)
Expand Down
82 changes: 82 additions & 0 deletions gossip/gasprice/constructive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package gasprice

import (
"math/big"

"github.com/Fantom-foundation/go-opera/utils/piecefunc"
)

func (gpo *Oracle) maxTotalGasPower() *big.Int {
rules := gpo.backend.GetRules()

allocBn := new(big.Int).SetUint64(rules.Economy.LongGasPower.AllocPerSec)
periodBn := new(big.Int).SetUint64(uint64(rules.Economy.LongGasPower.MaxAllocPeriod))
maxTotalGasPowerBn := new(big.Int).Mul(allocBn, periodBn)
maxTotalGasPowerBn.Div(maxTotalGasPowerBn, secondBn)
return maxTotalGasPowerBn
}

func (gpo *Oracle) effectiveMinGasPrice() *big.Int {
return gpo.constructiveGasPrice(0, 0, gpo.backend.GetRules().Economy.MinGasPrice)
}

func (gpo *Oracle) constructiveGasPrice(gasOffestAbs uint64, gasOffestRatio uint64, adjustedMinPrice *big.Int) *big.Int {
max := gpo.maxTotalGasPower()

current64 := gpo.backend.TotalGasPowerLeft()
if current64 > gasOffestAbs {
current64 -= gasOffestAbs
} else {
current64 = 0
}
current := new(big.Int).SetUint64(current64)

freeRatioBn := current.Mul(current, DecimalUnitBn)
freeRatioBn.Div(freeRatioBn, max)
freeRatio := freeRatioBn.Uint64()
if freeRatio > gasOffestRatio {
freeRatio -= gasOffestRatio
} else {
freeRatio = 0
}
if freeRatio > DecimalUnit {
freeRatio = DecimalUnit
}
v := gpo.constructiveGasPriceOf(freeRatio, adjustedMinPrice)
return v
}

var freeRatioToConstructiveGasRatio = piecefunc.NewFunc([]piecefunc.Dot{
{
X: 0,
Y: 25 * DecimalUnit,
},
{
X: 0.3 * DecimalUnit,
Y: 9 * DecimalUnit,
},
{
X: 0.5 * DecimalUnit,
Y: 3.75 * DecimalUnit,
},
{
X: 0.8 * DecimalUnit,
Y: 1.5 * DecimalUnit,
},
{
X: 0.95 * DecimalUnit,
Y: 1.05 * DecimalUnit,
},
{
X: DecimalUnit,
Y: DecimalUnit,
},
})

func (gpo *Oracle) constructiveGasPriceOf(freeRatio uint64, adjustedMinPrice *big.Int) *big.Int {
multiplier := new(big.Int).SetUint64(freeRatioToConstructiveGasRatio(freeRatio))

// gas price = multiplier * adjustedMinPrice
price := multiplier.Mul(multiplier, adjustedMinPrice)
return price.Div(price, DecimalUnitBn)
}
Loading

0 comments on commit f591585

Please sign in to comment.