diff --git a/ethapi/api.go b/ethapi/api.go index d24e2d908..70aa74320 100644 --- a/ethapi/api.go +++ b/ethapi/api.go @@ -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" ) @@ -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 { @@ -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) { @@ -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++ { @@ -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() diff --git a/ethapi/backend.go b/ethapi/backend.go index a96510bd2..214c939f0 100644 --- a/ethapi/backend.go +++ b/ethapi/backend.go @@ -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 diff --git a/ethapi/transaction_args.go b/ethapi/transaction_args.go index 4857a6b51..42363f283 100644 --- a/ethapi/transaction_args.go +++ b/ethapi/transaction_args.go @@ -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 @@ -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 { @@ -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) } diff --git a/eventcheck/gaspowercheck/gas_power_check.go b/eventcheck/gaspowercheck/gas_power_check.go index fe487e930..954ed5edf 100644 --- a/eventcheck/gaspowercheck/gas_power_check.go +++ b/eventcheck/gaspowercheck/gas_power_check.go @@ -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 { @@ -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, diff --git a/evmcore/tx_pool.go b/evmcore/tx_pool.go index 830cd589a..8f07673a2 100644 --- a/evmcore/tx_pool.go +++ b/evmcore/tx_pool.go @@ -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 @@ -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) } @@ -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 } diff --git a/evmcore/tx_pool_test.go b/evmcore/tx_pool_test.go index 3acd499b0..11532fe4b 100644 --- a/evmcore/tx_pool_test.go +++ b/evmcore/tx_pool_test.go @@ -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 { diff --git a/gossip/config.go b/gossip/config.go index ecba0c24c..420ecc459 100644 --- a/gossip/config.go +++ b/gossip/config.go @@ -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, diff --git a/gossip/dummy_tx_pool.go b/gossip/dummy_tx_pool.go index b9397ceeb..51f473244 100644 --- a/gossip/dummy_tx_pool.go +++ b/gossip/dummy_tx_pool.go @@ -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) } diff --git a/gossip/ethapi_backend.go b/gossip/ethapi_backend.go index a0803e5b6..a62f907a0 100644 --- a/gossip/ethapi_backend.go +++ b/gossip/ethapi_backend.go @@ -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 { diff --git a/gossip/evm_state_reader.go b/gossip/evm_state_reader.go index c80e016fb..11a138ebf 100644 --- a/gossip/evm_state_reader.go +++ b/gossip/evm_state_reader.go @@ -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 @@ -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) diff --git a/gossip/gasprice/constructive.go b/gossip/gasprice/constructive.go new file mode 100644 index 000000000..d7fb99aa2 --- /dev/null +++ b/gossip/gasprice/constructive.go @@ -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) +} diff --git a/gossip/gasprice/gasprice.go b/gossip/gasprice/gasprice.go index 7ccb0febc..872f29d6a 100644 --- a/gossip/gasprice/gasprice.go +++ b/gossip/gasprice/gasprice.go @@ -24,6 +24,8 @@ import ( "github.com/Fantom-foundation/lachesis-base/inter/idx" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" + lru "github.com/hashicorp/golang-lru" "github.com/Fantom-foundation/go-opera/opera" "github.com/Fantom-foundation/go-opera/utils/piecefunc" @@ -32,20 +34,21 @@ import ( "github.com/ethereum/go-ethereum/params" ) -var DefaultMaxTipCap = big.NewInt(10000000 * params.GWei) - -var secondBn = big.NewInt(int64(time.Second)) - -const DecimalUnit = piecefunc.DecimalUnit +var ( + DefaultMaxGasPrice = big.NewInt(10000000 * params.GWei) + DecimalUnitBn = big.NewInt(DecimalUnit) + secondBn = new(big.Int).SetUint64(uint64(time.Second)) +) -var DecimalUnitBn = big.NewInt(DecimalUnit) +const ( + AsDefaultCertainty = math.MaxUint64 + DecimalUnit = piecefunc.DecimalUnit +) type Config struct { - MaxTipCap *big.Int `toml:",omitempty"` - MinTipCap *big.Int `toml:",omitempty"` - MaxTipCapMultiplierRatio *big.Int `toml:",omitempty"` - MiddleTipCapMultiplierRatio *big.Int `toml:",omitempty"` - GasPowerWallRatio *big.Int `toml:",omitempty"` + MaxGasPrice *big.Int `toml:",omitempty"` + MinGasPrice *big.Int `toml:",omitempty"` + DefaultCertainty uint64 `toml:",omitempty"` } type Reader interface { @@ -53,9 +56,15 @@ type Reader interface { TotalGasPowerLeft() uint64 GetRules() opera.Rules GetPendingRules() opera.Rules + PendingTxs() types.Transactions } -type cache struct { +type tipCache struct { + upd time.Time + tip *big.Int +} + +type effectiveMinGasPriceCache struct { head idx.Block lock sync.RWMutex value *big.Int @@ -66,9 +75,15 @@ type cache struct { type Oracle struct { backend Reader + c circularTxpoolStats + cfg Config - cache cache + eCache effectiveMinGasPriceCache + tCache *lru.Cache + + wg sync.WaitGroup + quit chan struct{} } func sanitizeBigInt(val, min, max, _default *big.Int, name string) *big.Int { @@ -89,104 +104,111 @@ func sanitizeBigInt(val, min, max, _default *big.Int, name string) *big.Int { // NewOracle returns a new gasprice oracle which can recommend suitable // gasprice for newly created transaction. -func NewOracle(backend Reader, params Config) *Oracle { - params.MaxTipCap = sanitizeBigInt(params.MaxTipCap, nil, nil, DefaultMaxTipCap, "MaxTipCap") - params.MinTipCap = sanitizeBigInt(params.MinTipCap, nil, nil, new(big.Int), "MinTipCap") - params.GasPowerWallRatio = sanitizeBigInt(params.GasPowerWallRatio, big.NewInt(1), big.NewInt(DecimalUnit-2), big.NewInt(1), "GasPowerWallRatio") - params.MaxTipCapMultiplierRatio = sanitizeBigInt(params.MaxTipCapMultiplierRatio, DecimalUnitBn, nil, big.NewInt(10*DecimalUnit), "MaxTipCapMultiplierRatio") - params.MiddleTipCapMultiplierRatio = sanitizeBigInt(params.MiddleTipCapMultiplierRatio, DecimalUnitBn, params.MaxTipCapMultiplierRatio, big.NewInt(2*DecimalUnit), "MiddleTipCapMultiplierRatio") +func NewOracle(params Config) *Oracle { + params.MaxGasPrice = sanitizeBigInt(params.MaxGasPrice, nil, nil, DefaultMaxGasPrice, "MaxGasPrice") + params.MinGasPrice = sanitizeBigInt(params.MinGasPrice, nil, nil, new(big.Int), "MinGasPrice") + params.DefaultCertainty = sanitizeBigInt(new(big.Int).SetUint64(params.DefaultCertainty), big.NewInt(0), DecimalUnitBn, big.NewInt(DecimalUnit/2), "DefaultCertainty").Uint64() + tCache, _ := lru.New(100) return &Oracle{ - backend: backend, - cfg: params, + cfg: params, + tCache: tCache, + quit: make(chan struct{}), } } -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) Start(backend Reader) { + gpo.backend = backend + gpo.wg.Add(1) + go func() { + defer gpo.wg.Done() + gpo.txpoolStatsLoop() + }() } -func (gpo *Oracle) suggestTipCap() *big.Int { - max := gpo.maxTotalGasPower() - - current := new(big.Int).SetUint64(gpo.backend.TotalGasPowerLeft()) - - freeRatioBn := current.Mul(current, DecimalUnitBn) - freeRatioBn.Div(freeRatioBn, max) - freeRatio := freeRatioBn.Uint64() - if freeRatio > DecimalUnit { - freeRatio = DecimalUnit - } - - multiplierFn := piecefunc.NewFunc([]piecefunc.Dot{ - { - X: 0, - Y: gpo.cfg.MaxTipCapMultiplierRatio.Uint64(), - }, - { - X: gpo.cfg.GasPowerWallRatio.Uint64(), - Y: gpo.cfg.MaxTipCapMultiplierRatio.Uint64(), - }, - { - X: gpo.cfg.GasPowerWallRatio.Uint64() + (DecimalUnit-gpo.cfg.GasPowerWallRatio.Uint64())/2, - Y: gpo.cfg.MiddleTipCapMultiplierRatio.Uint64(), - }, - { - X: DecimalUnit, - Y: 0, - }, - }) - - multiplier := new(big.Int).SetUint64(multiplierFn(freeRatio)) +func (gpo *Oracle) Stop() { + close(gpo.quit) + gpo.wg.Wait() +} +func (gpo *Oracle) suggestTip(certainty uint64) *big.Int { minPrice := gpo.backend.GetRules().Economy.MinGasPrice pendingMinPrice := gpo.backend.GetPendingRules().Economy.MinGasPrice - adjustedMinPrice := math.BigMax(minPrice, pendingMinPrice) + adjustedMinGasPrice := math.BigMax(minPrice, pendingMinPrice) - // tip cap = (multiplier * adjustedMinPrice + adjustedMinPrice) - minPrice - tip := multiplier.Mul(multiplier, adjustedMinPrice) - tip.Div(tip, DecimalUnitBn) - tip.Add(tip, adjustedMinPrice) - tip.Sub(tip, minPrice) + reactive := gpo.reactiveGasPrice(certainty) + constructive := gpo.constructiveGasPrice(gpo.c.totalGas(), 0.005*DecimalUnit+certainty/25, adjustedMinGasPrice) - if tip.Cmp(gpo.cfg.MinTipCap) < 0 { - return gpo.cfg.MinTipCap + combined := math.BigMax(reactive, constructive) + if combined.Cmp(gpo.cfg.MinGasPrice) < 0 { + combined = gpo.cfg.MinGasPrice } - if tip.Cmp(gpo.cfg.MaxTipCap) > 0 { - return gpo.cfg.MaxTipCap + if combined.Cmp(gpo.cfg.MaxGasPrice) > 0 { + combined = gpo.cfg.MaxGasPrice + } + + tip := new(big.Int).Sub(combined, minPrice) + if tip.Sign() < 0 { + return new(big.Int) } return tip } -// SuggestTipCap returns a tip cap so that newly created transaction can have a +// SuggestTip returns a tip cap so that newly created transaction can have a // very high chance to be included in the following blocks. // // Note, for legacy transactions and the legacy eth_gasPrice RPC call, it will be // necessary to add the basefee to the returned number to fall back to the legacy // behavior. -func (gpo *Oracle) SuggestTipCap() *big.Int { +func (gpo *Oracle) SuggestTip(certainty uint64) *big.Int { + if gpo.backend == nil { + return new(big.Int) + } + if certainty == AsDefaultCertainty { + certainty = gpo.cfg.DefaultCertainty + } + + const cacheSlack = DecimalUnit * 0.05 + roundedCertainty := certainty / cacheSlack + if cached, ok := gpo.tCache.Get(roundedCertainty); ok { + cache := cached.(tipCache) + if time.Since(cache.upd) < statUpdatePeriod { + return new(big.Int).Set(cache.tip) + } else { + gpo.tCache.Remove(roundedCertainty) + } + } + + tip := gpo.suggestTip(certainty) + + gpo.tCache.Add(roundedCertainty, tipCache{ + upd: time.Now(), + tip: tip, + }) + return new(big.Int).Set(tip) +} + +// EffectiveMinGasPrice returns softly enforced minimum gas price on top of on-chain minimum gas price (base fee) +func (gpo *Oracle) EffectiveMinGasPrice() *big.Int { + if gpo.backend == nil { + return new(big.Int).Set(gpo.cfg.MinGasPrice) + } head := gpo.backend.GetLatestBlockIndex() // If the latest gasprice is still available, return it. - gpo.cache.lock.RLock() - cachedHead, cachedValue := gpo.cache.head, gpo.cache.value - gpo.cache.lock.RUnlock() + gpo.eCache.lock.RLock() + cachedHead, cachedValue := gpo.eCache.head, gpo.eCache.value + gpo.eCache.lock.RUnlock() if head <= cachedHead { return new(big.Int).Set(cachedValue) } - value := gpo.suggestTipCap() + value := gpo.effectiveMinGasPrice() - gpo.cache.lock.Lock() - if head > gpo.cache.head { - gpo.cache.head = head - gpo.cache.value = value + gpo.eCache.lock.Lock() + if head > gpo.eCache.head { + gpo.eCache.head = head + gpo.eCache.value = value } - gpo.cache.lock.Unlock() + gpo.eCache.lock.Unlock() return new(big.Int).Set(value) } diff --git a/gossip/gasprice/gasprice_test.go b/gossip/gasprice/gasprice_test.go index 28fae52f5..f6b4af260 100644 --- a/gossip/gasprice/gasprice_test.go +++ b/gossip/gasprice/gasprice_test.go @@ -5,16 +5,25 @@ import ( "testing" "github.com/Fantom-foundation/lachesis-base/inter/idx" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" "github.com/Fantom-foundation/go-opera/opera" ) +type fakeTx struct { + gas uint64 + tip *big.Int + cap *big.Int +} + type TestBackend struct { block idx.Block totalGasPowerLeft uint64 rules opera.Rules pendingRules opera.Rules + pendingTxs []fakeTx } func (t TestBackend) GetLatestBlockIndex() idx.Block { @@ -33,34 +42,19 @@ func (t TestBackend) GetPendingRules() opera.Rules { return t.pendingRules } -func TestConstructor(t *testing.T) { - gpo := NewOracle(nil, Config{}) - require.Equal(t, "0", gpo.cfg.MinTipCap.String()) - require.Equal(t, DefaultMaxTipCap.String(), gpo.cfg.MaxTipCap.String()) - require.Equal(t, big.NewInt(2*DecimalUnit).String(), gpo.cfg.MiddleTipCapMultiplierRatio.String()) - require.Equal(t, big.NewInt(10*DecimalUnit).String(), gpo.cfg.MaxTipCapMultiplierRatio.String()) - require.Equal(t, "1", gpo.cfg.GasPowerWallRatio.String()) - - gpo = NewOracle(nil, Config{ - GasPowerWallRatio: big.NewInt(2 * DecimalUnit), - }) - require.Equal(t, "999998", gpo.cfg.GasPowerWallRatio.String()) - - gpo = NewOracle(nil, Config{ - MiddleTipCapMultiplierRatio: big.NewInt(0.5 * DecimalUnit), - MaxTipCapMultiplierRatio: big.NewInt(0.5 * DecimalUnit), - }) - require.Equal(t, DecimalUnitBn.String(), gpo.cfg.MiddleTipCapMultiplierRatio.String()) - require.Equal(t, DecimalUnitBn.String(), gpo.cfg.MaxTipCapMultiplierRatio.String()) - - gpo = NewOracle(nil, Config{ - MiddleTipCapMultiplierRatio: big.NewInt(3 * DecimalUnit), - MaxTipCapMultiplierRatio: big.NewInt(2 * DecimalUnit), - }) - require.Equal(t, gpo.cfg.MaxTipCapMultiplierRatio.String(), gpo.cfg.MiddleTipCapMultiplierRatio.String()) +func (t TestBackend) PendingTxs() types.Transactions { + txs := make(types.Transactions, 0, len(t.pendingTxs)) + for _, tx := range t.pendingTxs { + txs = append(txs, types.NewTx(&types.DynamicFeeTx{ + GasTipCap: tx.tip, + GasFeeCap: tx.cap, + Gas: tx.gas, + })) + } + return txs } -func TestSuggestTipCap(t *testing.T) { +func TestOracle_EffectiveMinGasPrice(t *testing.T) { backend := &TestBackend{ block: 1, totalGasPowerLeft: 0, @@ -68,87 +62,253 @@ func TestSuggestTipCap(t *testing.T) { pendingRules: opera.FakeNetRules(), } - gpo := NewOracle(backend, Config{}) + gpo := NewOracle(Config{}) + gpo.cfg.MaxGasPrice = math.MaxBig256 + gpo.cfg.MinGasPrice = new(big.Int) - maxMul := big.NewInt(9 * DecimalUnit) - gpo.cfg.MiddleTipCapMultiplierRatio = big.NewInt(DecimalUnit) - gpo.cfg.MaxTipCapMultiplierRatio = maxMul + // no backend + require.Equal(t, "0", gpo.EffectiveMinGasPrice().String()) + gpo.backend = backend // all the gas is consumed, price should be high - require.Equal(t, "9000000000", gpo.SuggestTipCap().String()) - - // increase MaxTipCapMultiplierRatio - maxMul = big.NewInt(100 * DecimalUnit) - gpo.cfg.MaxTipCapMultiplierRatio = maxMul + backend.block++ + backend.totalGasPowerLeft = 0 + require.Equal(t, "25000000000", gpo.EffectiveMinGasPrice().String()) // test the cache as well - require.Equal(t, "9000000000", gpo.SuggestTipCap().String()) + backend.totalGasPowerLeft = 1008000000 + require.Equal(t, "25000000000", gpo.EffectiveMinGasPrice().String()) backend.block++ - require.Equal(t, "100000000000", gpo.SuggestTipCap().String()) + require.Equal(t, "24994672000", gpo.EffectiveMinGasPrice().String()) backend.block++ // all the gas is free, price should be low backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() require.Equal(t, uint64(0x92aeed1c000), backend.totalGasPowerLeft) - require.Equal(t, "0", gpo.SuggestTipCap().String()) + require.Equal(t, "1000000000", gpo.EffectiveMinGasPrice().String()) backend.block++ // edge case with totalGasPowerLeft exceeding maxTotalGasPower backend.totalGasPowerLeft = 2 * gpo.maxTotalGasPower().Uint64() - require.Equal(t, "0", gpo.SuggestTipCap().String()) + require.Equal(t, "1000000000", gpo.EffectiveMinGasPrice().String()) backend.block++ - // half of the gas is free, price should be 2x + // half of the gas is free, price should be 3.75x backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() / 2 - require.Equal(t, "1000000000", gpo.SuggestTipCap().String()) + require.Equal(t, "3750000000", gpo.EffectiveMinGasPrice().String()) backend.block++ // third of the gas is free, price should be higher backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() / 3 - require.Equal(t, "34000165000", gpo.SuggestTipCap().String()) + require.Equal(t, "8125008000", gpo.EffectiveMinGasPrice().String()) backend.block++ - // check the 5% wall - gpo.cfg.GasPowerWallRatio = big.NewInt(DecimalUnit / 20) - require.Equal(t, "40947490000", gpo.SuggestTipCap().String()) + // check min and max price hard limits don't apply + gpo.cfg.MaxGasPrice = big.NewInt(2000000000) + backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() / 3 + require.Equal(t, "8125008000", gpo.EffectiveMinGasPrice().String()) backend.block++ - // check the 10% wall - gpo.cfg.GasPowerWallRatio = big.NewInt(DecimalUnit / 10) - require.Equal(t, "48666817000", gpo.SuggestTipCap().String()) + gpo.cfg.MinGasPrice = big.NewInt(1500000000) + backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() + require.Equal(t, "1000000000", gpo.EffectiveMinGasPrice().String()) backend.block++ +} - // check the 20% wall - gpo.cfg.GasPowerWallRatio = big.NewInt(DecimalUnit / 5) - require.Equal(t, "67000132000", gpo.SuggestTipCap().String()) - backend.block++ +func TestOracle_constructiveGasPrice(t *testing.T) { + backend := &TestBackend{ + totalGasPowerLeft: 0, + rules: opera.FakeNetRules(), + pendingRules: opera.FakeNetRules(), + } - // check the 33.3% wall - gpo.cfg.GasPowerWallRatio = big.NewInt(DecimalUnit * 0.333) - require.Equal(t, "99901198000", gpo.SuggestTipCap().String()) - backend.block++ + gpo := NewOracle(Config{}) + gpo.backend = backend + gpo.cfg.MaxGasPrice = math.MaxBig256 + gpo.cfg.MinGasPrice = new(big.Int) - // check the 50.0% wall - gpo.cfg.GasPowerWallRatio = big.NewInt(DecimalUnit / 2) - require.Equal(t, "100000000000", gpo.SuggestTipCap().String()) - backend.block++ + // all the gas is consumed, price should be high + backend.totalGasPowerLeft = 0 + require.Equal(t, "2500", gpo.constructiveGasPrice(0, 0, big.NewInt(100)).String()) + require.Equal(t, "2500", gpo.constructiveGasPrice(0, 0.1*DecimalUnit, big.NewInt(100)).String()) + require.Equal(t, "2500", gpo.constructiveGasPrice(1008000000, 0, big.NewInt(100)).String()) + require.Equal(t, "2500", gpo.constructiveGasPrice(gpo.maxTotalGasPower().Uint64()*2, 2*DecimalUnit, big.NewInt(100)).String()) - // check the maximum wall - gpo.cfg.GasPowerWallRatio = NewOracle(nil, Config{ - GasPowerWallRatio: DecimalUnitBn, - }).cfg.GasPowerWallRatio - require.Equal(t, "100000000000", gpo.SuggestTipCap().String()) - backend.block++ + // all the gas is free, price should be low + backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() + require.Equal(t, "100", gpo.constructiveGasPrice(0, 0, big.NewInt(100)).String()) + require.Equal(t, "120", gpo.constructiveGasPrice(0, 0.1*DecimalUnit, big.NewInt(100)).String()) + require.Equal(t, "101", gpo.constructiveGasPrice(100800000000, 0, big.NewInt(100)).String()) + require.Equal(t, "2500", gpo.constructiveGasPrice(gpo.maxTotalGasPower().Uint64()*2, 2*DecimalUnit, big.NewInt(100)).String()) - // check max price hard limit - gpo.cfg.MaxTipCap = big.NewInt(2000000000) + // half of the gas is free, price should be 3.75x + backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() / 2 + require.Equal(t, "375", gpo.constructiveGasPrice(0, 0, big.NewInt(100)).String()) + require.Equal(t, "637", gpo.constructiveGasPrice(0, 0.1*DecimalUnit, big.NewInt(100)).String()) + require.Equal(t, "401", gpo.constructiveGasPrice(100800000000, 0, big.NewInt(100)).String()) + require.Equal(t, "2500", gpo.constructiveGasPrice(gpo.maxTotalGasPower().Uint64()*2, 2*DecimalUnit, big.NewInt(100)).String()) + + // third of the gas is free, price should be higher backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() / 3 - require.Equal(t, "2000000000", gpo.SuggestTipCap().String()) - backend.block++ + require.Equal(t, "812", gpo.constructiveGasPrice(0, 0, big.NewInt(100)).String()) + require.Equal(t, "1255", gpo.constructiveGasPrice(0, 0.1*DecimalUnit, big.NewInt(100)).String()) + require.Equal(t, "838", gpo.constructiveGasPrice(100800000000, 0, big.NewInt(100)).String()) + require.Equal(t, "2500", gpo.constructiveGasPrice(gpo.maxTotalGasPower().Uint64()*2, 2*DecimalUnit, big.NewInt(100)).String()) - // check min price hard limit - gpo.cfg.MinTipCap = big.NewInt(1500000000) - backend.totalGasPowerLeft = gpo.maxTotalGasPower().Uint64() - require.Equal(t, "1500000000", gpo.SuggestTipCap().String()) - backend.block++ +} + +func TestOracle_reactiveGasPrice(t *testing.T) { + backend := &TestBackend{ + totalGasPowerLeft: 0, + rules: opera.FakeNetRules(), + pendingRules: opera.FakeNetRules(), + } + + gpo := NewOracle(Config{}) + gpo.backend = backend + gpo.cfg.MaxGasPrice = math.MaxBig256 + gpo.cfg.MinGasPrice = new(big.Int) + + // no stats -> zero price + gpo.c = circularTxpoolStats{} + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(DecimalUnit).String()) + + // one tx + gpo.c = circularTxpoolStats{} + backend.pendingTxs = append(backend.pendingTxs, fakeTx{ + gas: 50000, + tip: big.NewInt(0), + cap: big.NewInt(1e9), + }) + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "200000000", gpo.reactiveGasPrice(0.9*DecimalUnit).String()) + require.Equal(t, "600000000", gpo.reactiveGasPrice(0.95*DecimalUnit).String()) + require.Equal(t, "920000000", gpo.reactiveGasPrice(0.99*DecimalUnit).String()) + require.Equal(t, "1000000000", gpo.reactiveGasPrice(DecimalUnit).String()) + + // add one more tx + backend.pendingTxs = append(backend.pendingTxs, fakeTx{ + gas: 25000, + tip: big.NewInt(3 * 1e9), + cap: big.NewInt(3.5 * 1e9), + }) + + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "1000000000", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "450000000", gpo.reactiveGasPrice(0.9*DecimalUnit).String()) + require.Equal(t, "1350000000", gpo.reactiveGasPrice(0.95*DecimalUnit).String()) + require.Equal(t, "2070000000", gpo.reactiveGasPrice(0.99*DecimalUnit).String()) + require.Equal(t, "2250000000", gpo.reactiveGasPrice(DecimalUnit).String()) + + // add two more txs + backend.pendingTxs = append(backend.pendingTxs, fakeTx{ + gas: 2500000, + tip: big.NewInt(1 * 1e9), + cap: big.NewInt(3.5 * 1e9), + }) + backend.pendingTxs = append(backend.pendingTxs, fakeTx{ + gas: 2500000, + tip: big.NewInt(0 * 1e9), + cap: big.NewInt(3.5 * 1e9), + }) + + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0).String()) + require.Equal(t, "333333333", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "799999999", gpo.reactiveGasPrice(0.9*DecimalUnit).String()) + require.Equal(t, "1733333332", gpo.reactiveGasPrice(0.95*DecimalUnit).String()) + require.Equal(t, "2479999999", gpo.reactiveGasPrice(0.99*DecimalUnit).String()) + require.Equal(t, "2666666666", gpo.reactiveGasPrice(DecimalUnit).String()) + // price gets closer to latest state + gpo.txpoolStatsTick() + require.Equal(t, "500000000", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "2875000000", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "600000000", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000000", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "666666666", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3083333333", gpo.reactiveGasPrice(DecimalUnit).String()) + for i := 0; i < statsBuffer - 5; i++ { + gpo.txpoolStatsTick() + } + require.Equal(t, "916666666", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3500000000", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "1000000000", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3500000000", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "1000000000", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3500000000", gpo.reactiveGasPrice(DecimalUnit).String()) + + // change minGasPrice + backend.rules.Economy.MinGasPrice = big.NewInt(100) + gpo.txpoolStatsTick() + require.Equal(t, "916666675", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3458333341", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "833333350", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3416666683", gpo.reactiveGasPrice(DecimalUnit).String()) + for i := 0; i < statsBuffer - 3; i++ { + gpo.txpoolStatsTick() + } + require.Equal(t, "83333425", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3041666758", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "100", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "100", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + + // half of txs are confirmed now + backend.pendingTxs = backend.pendingTxs[:2] + gpo.txpoolStatsTick() + require.Equal(t, "91", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "83", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + for i := 0; i < statsBuffer - 3; i++ { + gpo.txpoolStatsTick() + } + require.Equal(t, "8", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + + // all txs are confirmed now + backend.pendingTxs = backend.pendingTxs[:0] + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + for i := 0; i < statsBuffer - 3; i++ { + gpo.txpoolStatsTick() + } + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "3000000100", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(DecimalUnit).String()) + gpo.txpoolStatsTick() + require.Equal(t, "0", gpo.reactiveGasPrice(0.8*DecimalUnit).String()) + require.Equal(t, "0", gpo.reactiveGasPrice(DecimalUnit).String()) } diff --git a/gossip/gasprice/reactive.go b/gossip/gasprice/reactive.go new file mode 100644 index 000000000..4c4464fbb --- /dev/null +++ b/gossip/gasprice/reactive.go @@ -0,0 +1,214 @@ +package gasprice + +import ( + "math/big" + "sort" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/core/types" + + "github.com/Fantom-foundation/go-opera/utils/piecefunc" +) + +const ( + percentilesPerStat = 20 + statUpdatePeriod = 1 * time.Second + statsBuffer = int((12 * time.Second) / statUpdatePeriod) + maxGasToIndex = 40000000 +) + +type txpoolStat struct { + totalGas uint64 + percentiles [percentilesPerStat]*big.Int +} + +type circularTxpoolStats struct { + stats [statsBuffer]txpoolStat + i int + activated uint32 + avg atomic.Value +} + +var certaintyToGasAbove = piecefunc.NewFunc([]piecefunc.Dot{ + { + X: 0, + Y: 50000000, + }, + { + X: 0.2 * DecimalUnit, + Y: 20000000, + }, + { + X: 0.5 * DecimalUnit, + Y: 8000000, + }, + { + X: DecimalUnit, + Y: 0, + }, +}) + +func (gpo *Oracle) reactiveGasPrice(certainty uint64) *big.Int { + gasAbove := certaintyToGasAbove(certainty) + + return gpo.c.getGasPriceForGasAbove(gasAbove) +} + +func (gpo *Oracle) txpoolStatsTick() { + c := &gpo.c + // calculate txpool statistic and push into the circular buffer + c.stats[c.i] = gpo.calcTxpoolStat() + c.i = (c.i + 1) % len(c.stats) + // calculate average of statistics in the circular buffer + c.avg.Store(c.calcAvg()) +} + +func (gpo *Oracle) txpoolStatsLoop() { + ticker := time.NewTicker(statUpdatePeriod) + defer ticker.Stop() + for i := uint32(0); ; i++ { + select { + case <-ticker.C: + // calculate more frequently after first request + if atomic.LoadUint32(&gpo.c.activated) != 0 || i%5 == 0 { + gpo.txpoolStatsTick() + } + case <-gpo.quit: + return + } + } +} + +// calcAvg calculates average of statistics in the circular buffer +func (c *circularTxpoolStats) calcAvg() txpoolStat { + avg := txpoolStat{} + for p := range avg.percentiles { + avg.percentiles[p] = new(big.Int) + } + nonZero := uint64(0) + for _, s := range c.stats { + if s.totalGas == 0 { + continue + } + nonZero++ + avg.totalGas += s.totalGas + for p := range s.percentiles { + if s.percentiles[p] != nil { + avg.percentiles[p].Add(avg.percentiles[p], s.percentiles[p]) + } + } + } + if nonZero == 0 { + return avg + } + avg.totalGas /= nonZero + nonZeroBn := new(big.Int).SetUint64(nonZero) + for p := range avg.percentiles { + avg.percentiles[p].Div(avg.percentiles[p], nonZeroBn) + } + return avg +} + +func (c *circularTxpoolStats) getGasPriceForGasAbove(gas uint64) *big.Int { + atomic.StoreUint32(&c.activated, 1) + avg_c := c.avg.Load() + if avg_c == nil { + return new(big.Int) + } + avg := avg_c.(txpoolStat) + if avg.totalGas == 0 { + return new(big.Int) + } + if gas > maxGasToIndex { + // extrapolate linearly + v := new(big.Int).Mul(avg.percentiles[len(avg.percentiles)-1], new(big.Int).SetUint64(maxGasToIndex)) + v.Div(v, new(big.Int).SetUint64(gas+1)) + return v + } + p0 := gas * uint64(len(avg.percentiles)) / maxGasToIndex + if p0 >= uint64(len(avg.percentiles))-1 { + return avg.percentiles[len(avg.percentiles)-1] + } + // interpolate linearly + p1 := p0 + 1 + x := gas + x0, x1 := p0*maxGasToIndex/uint64(len(avg.percentiles)), p1*maxGasToIndex/uint64(len(avg.percentiles)) + y0, y1 := avg.percentiles[p0], avg.percentiles[p1] + return div64I(addBigI(mul64N(y0, x1-x), mul64N(y1, x-x0)), x1-x0) +} + +func mul64N(a *big.Int, b uint64) *big.Int { + return new(big.Int).Mul(a, new(big.Int).SetUint64(b)) +} + +func div64I(a *big.Int, b uint64) *big.Int { + return a.Div(a, new(big.Int).SetUint64(b)) +} + +func addBigI(a, b *big.Int) *big.Int { + return a.Add(a, b) +} + +func (c *circularTxpoolStats) totalGas() uint64 { + atomic.StoreUint32(&c.activated, 1) + avgC := c.avg.Load() + if avgC == nil { + return 0 + } + avg := avgC.(txpoolStat) + return avg.totalGas +} + +// calcTxpoolStat retrieves txpool transactions and calculates statistics +func (gpo *Oracle) calcTxpoolStat() txpoolStat { + txs := gpo.backend.PendingTxs() + s := txpoolStat{} + if len(txs) == 0 { + // short circuit if empty txpool + return s + } + // don't index more transactions than needed for GPO purposes + const maxTxsToIndex = 400 + + minGasPrice := gpo.backend.GetRules().Economy.MinGasPrice + // txs are sorted from large price to small + sorted := txs + sort.Slice(sorted, func(i, j int) bool { + a, b := sorted[i], sorted[j] + return a.EffectiveGasTipCmp(b, minGasPrice) < 0 + }) + + if len(txs) > maxTxsToIndex { + txs = txs[:maxTxsToIndex] + } + sortedDown := make(types.Transactions, len(sorted)) + for i, tx := range sorted { + sortedDown[len(sorted)-1-i] = tx + } + + for i, tx := range sortedDown { + s.totalGas += tx.Gas() + if s.totalGas > maxGasToIndex { + sortedDown = sortedDown[:i+1] + break + } + } + + gasCounter := uint64(0) + p := uint64(0) + for _, tx := range sortedDown { + for p < uint64(len(s.percentiles)) && gasCounter >= p*maxGasToIndex/uint64(len(s.percentiles)) { + s.percentiles[p] = tx.EffectiveGasTipValue(minGasPrice) + if s.percentiles[p].Sign() < 0 { + s.percentiles[p] = minGasPrice + } else { + s.percentiles[p].Add(s.percentiles[p], minGasPrice) + } + p++ + } + gasCounter += tx.Gas() + } + + return s +} diff --git a/gossip/gpo_backend.go b/gossip/gpo_backend.go index f2f7d3b48..eea1bfb88 100644 --- a/gossip/gpo_backend.go +++ b/gossip/gpo_backend.go @@ -3,14 +3,17 @@ package gossip import ( "github.com/Fantom-foundation/lachesis-base/hash" "github.com/Fantom-foundation/lachesis-base/inter/idx" + "github.com/ethereum/go-ethereum/core/types" + "github.com/Fantom-foundation/go-opera/eventcheck/gaspowercheck" "github.com/Fantom-foundation/go-opera/inter" "github.com/Fantom-foundation/go-opera/opera" "github.com/Fantom-foundation/go-opera/utils/concurrent" ) type GPOBackend struct { - store *Store + store *Store + txpool TxPool } func (b *GPOBackend) GetLatestBlockIndex() idx.Block { @@ -29,9 +32,13 @@ func (b *GPOBackend) GetPendingRules() opera.Rules { return es.Rules } +func (b *GPOBackend) PendingTxs() types.Transactions { + return b.txpool.PendingSlice() +} + // TotalGasPowerLeft returns a total amount of obtained gas power by the validators, according to the latest events from each validator func (b *GPOBackend) TotalGasPowerLeft() uint64 { - es := b.store.GetEpochState() + bs, es := b.store.GetBlockEpochState() set := b.store.GetLastEvents(es.Epoch) if set == nil { set = concurrent.WrapValidatorEventsSet(map[idx.ValidatorID]hash.Event{}) @@ -40,17 +47,43 @@ func (b *GPOBackend) TotalGasPowerLeft() uint64 { defer set.RUnlock() metValidators := map[idx.ValidatorID]bool{} total := uint64(0) + gasPowerCheckCfg := gaspowercheck.Config{ + Idx: inter.LongTermGas, + AllocPerSec: es.Rules.Economy.LongGasPower.AllocPerSec, + MaxAllocPeriod: es.Rules.Economy.LongGasPower.MaxAllocPeriod, + MinEnsuredAlloc: es.Rules.Economy.Gas.MaxEventGas, + StartupAllocPeriod: es.Rules.Economy.LongGasPower.StartupAllocPeriod, + MinStartupGas: es.Rules.Economy.LongGasPower.MinStartupGas, + } // count GasPowerLeft from latest events of this epoch for _, tip := range set.Val { e := b.store.GetEvent(tip) - total += e.GasPowerLeft().Gas[inter.LongTermGas] + left := e.GasPowerLeft().Gas[inter.LongTermGas] + left += bs.GetValidatorState(e.Creator(), es.Validators).DirtyGasRefund + + _, max, _ := gaspowercheck.CalcValidatorGasPowerPerSec(e.Creator(), es.Validators, gasPowerCheckCfg) + if left > max { + left = max + } + total += left + metValidators[e.Creator()] = true } // count GasPowerLeft from last events of prev epoch if no event in current epoch is present for i := idx.Validator(0); i < es.Validators.Len(); i++ { vid := es.Validators.GetID(i) if !metValidators[vid] { - total += es.ValidatorStates[i].PrevEpochEvent.GasPowerLeft.Gas[inter.LongTermGas] + left := es.ValidatorStates[i].PrevEpochEvent.GasPowerLeft.Gas[inter.LongTermGas] + left += es.ValidatorStates[i].GasRefund + + _, max, startup := gaspowercheck.CalcValidatorGasPowerPerSec(vid, es.Validators, gasPowerCheckCfg) + if left > max { + left = max + } + if left < startup { + left = startup + } + total += left } } diff --git a/gossip/protocol.go b/gossip/protocol.go index 8deca0edc..3753b2624 100644 --- a/gossip/protocol.go +++ b/gossip/protocol.go @@ -116,6 +116,7 @@ type TxPool interface { Stats() (int, int) Content() (map[common.Address]types.Transactions, map[common.Address]types.Transactions) ContentFrom(addr common.Address) (types.Transactions, types.Transactions) + PendingSlice() types.Transactions } // handshakeData is the network packet for the initial handshake message diff --git a/gossip/service.go b/gossip/service.go index cc785e32b..51490e00a 100644 --- a/gossip/service.go +++ b/gossip/service.go @@ -210,7 +210,7 @@ func newService(config Config, store *Store, blockProc BlockProc, engine lachesi svc.store.GetLlrState() // create GPO - svc.gpo = gasprice.NewOracle(&GPOBackend{store}, svc.config.GPO) + svc.gpo = gasprice.NewOracle(svc.config.GPO) // create checkers net := store.GetRules() @@ -419,6 +419,7 @@ func (s *Service) APIs() []rpc.API { // Start method invoked when the node is ready to start the service. func (s *Service) Start() error { + s.gpo.Start(&GPOBackend{s.store, s.txpool}) // start tflusher before starting snapshots generation s.tflusher.Start() // start snapshots generation @@ -476,6 +477,7 @@ func (s *Service) Stop() error { s.handler.Stop() s.feed.scope.Close() s.eventMux.Stop() + s.gpo.Stop() // it's safe to stop tflusher only before locking engineMu s.tflusher.Stop()