github.com/core-coin/go-core/v2@v2.1.9/xcb/energyprice/energyprice.go (about) 1 // Copyright 2023 by the Authors 2 // This file is part of the go-core library. 3 // 4 // The go-core library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-core library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-core library. If not, see <http://www.gnu.org/licenses/>. 16 17 package energyprice 18 19 import ( 20 "context" 21 "math/big" 22 "sort" 23 "sync" 24 25 "github.com/core-coin/go-core/v2/common" 26 "github.com/core-coin/go-core/v2/core/types" 27 "github.com/core-coin/go-core/v2/log" 28 "github.com/core-coin/go-core/v2/params" 29 "github.com/core-coin/go-core/v2/rpc" 30 ) 31 32 const sampleNumber = 3 // Number of transactions sampled in a block 33 34 var DefaultMaxPrice = big.NewInt(500 * params.Nucle) 35 36 type Config struct { 37 Blocks int 38 Percentile int 39 Default *big.Int `toml:",omitempty"` 40 MaxPrice *big.Int `toml:",omitempty"` 41 } 42 43 // OracleBackend includes all necessary background APIs for oracle. 44 type OracleBackend interface { 45 HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) 46 BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) 47 ChainConfig() *params.ChainConfig 48 } 49 50 // Oracle recommends energy prices based on the content of recent 51 // blocks. Suitable for both light and full clients. 52 type Oracle struct { 53 backend OracleBackend 54 lastHead common.Hash 55 lastPrice *big.Int 56 maxPrice *big.Int 57 cacheLock sync.RWMutex 58 fetchLock sync.Mutex 59 60 checkBlocks int 61 percentile int 62 } 63 64 // NewOracle returns a new energyprice oracle which can recommend suitable 65 // energyprice for newly created transaction. 66 func NewOracle(backend OracleBackend, params Config) *Oracle { 67 blocks := params.Blocks 68 if blocks < 1 { 69 blocks = 1 70 log.Warn("Sanitizing invalid energyprice oracle sample blocks", "provided", params.Blocks, "updated", blocks) 71 } 72 percent := params.Percentile 73 if percent < 0 { 74 percent = 0 75 log.Warn("Sanitizing invalid energyprice oracle sample percentile", "provided", params.Percentile, "updated", percent) 76 } 77 if percent > 100 { 78 percent = 100 79 log.Warn("Sanitizing invalid energyprice oracle sample percentile", "provided", params.Percentile, "updated", percent) 80 } 81 maxPrice := params.MaxPrice 82 if maxPrice == nil || maxPrice.Int64() <= 0 { 83 maxPrice = DefaultMaxPrice 84 log.Warn("Sanitizing invalid energyprice oracle price cap", "provided", params.MaxPrice, "updated", maxPrice) 85 } 86 return &Oracle{ 87 backend: backend, 88 lastPrice: params.Default, 89 maxPrice: maxPrice, 90 checkBlocks: blocks, 91 percentile: percent, 92 } 93 } 94 95 // SuggestPrice returns a energyprice so that newly created transaction can 96 // have a very high chance to be included in the following blocks. 97 func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) { 98 head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber) 99 headHash := head.Hash() 100 101 // If the latest energyprice is still available, return it. 102 gpo.cacheLock.RLock() 103 lastHead, lastPrice := gpo.lastHead, gpo.lastPrice 104 gpo.cacheLock.RUnlock() 105 if headHash == lastHead { 106 return lastPrice, nil 107 } 108 gpo.fetchLock.Lock() 109 defer gpo.fetchLock.Unlock() 110 111 // Try checking the cache again, maybe the last fetch fetched what we need 112 gpo.cacheLock.RLock() 113 lastHead, lastPrice = gpo.lastHead, gpo.lastPrice 114 gpo.cacheLock.RUnlock() 115 if headHash == lastHead { 116 return lastPrice, nil 117 } 118 var ( 119 sent, exp int 120 number = head.Number.Uint64() 121 result = make(chan getBlockPricesResult, gpo.checkBlocks) 122 quit = make(chan struct{}) 123 txPrices []*big.Int 124 ) 125 for sent < gpo.checkBlocks && number > 0 { 126 go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig().NetworkID), number, sampleNumber, result, quit) 127 sent++ 128 exp++ 129 number-- 130 } 131 for exp > 0 { 132 res := <-result 133 if res.err != nil { 134 close(quit) 135 return lastPrice, res.err 136 } 137 exp-- 138 // Nothing returned. There are two special cases here: 139 // - The block is empty 140 // - All the transactions included are sent by the miner itself. 141 // In these cases, use the latest calculated price for samping. 142 if len(res.prices) == 0 { 143 res.prices = []*big.Int{lastPrice} 144 } 145 // Besides, in order to collect enough data for sampling, if nothing 146 // meaningful returned, try to query more blocks. But the maximum 147 // is 2*checkBlocks. 148 if len(res.prices) == 1 && len(txPrices)+1+exp < gpo.checkBlocks*2 && number > 0 { 149 go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig().NetworkID), number, sampleNumber, result, quit) 150 sent++ 151 exp++ 152 number-- 153 } 154 txPrices = append(txPrices, res.prices...) 155 } 156 price := lastPrice 157 if len(txPrices) > 0 { 158 sort.Sort(bigIntArray(txPrices)) 159 price = txPrices[(len(txPrices)-1)*gpo.percentile/100] 160 } 161 if price.Cmp(gpo.maxPrice) > 0 { 162 price = new(big.Int).Set(gpo.maxPrice) 163 } 164 gpo.cacheLock.Lock() 165 gpo.lastHead = headHash 166 gpo.lastPrice = price 167 gpo.cacheLock.Unlock() 168 return price, nil 169 } 170 171 type getBlockPricesResult struct { 172 prices []*big.Int 173 err error 174 } 175 176 type transactionsByEnergyPrice []*types.Transaction 177 178 func (t transactionsByEnergyPrice) Len() int { return len(t) } 179 func (t transactionsByEnergyPrice) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 180 func (t transactionsByEnergyPrice) Less(i, j int) bool { return t[i].EnergyPriceCmp(t[j]) < 0 } 181 182 // getBlockPrices calculates the lowest transaction energy price in a given block 183 // and sends it to the result channel. If the block is empty or all transactions 184 // are sent by the miner itself(it doesn't make any sense to include this kind of 185 // transaction prices for sampling), nil energyprice is returned. 186 func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, blockNum uint64, limit int, result chan getBlockPricesResult, quit chan struct{}) { 187 block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum)) 188 if block == nil { 189 select { 190 case result <- getBlockPricesResult{nil, err}: 191 case <-quit: 192 } 193 return 194 } 195 blockTxs := block.Transactions() 196 txs := make([]*types.Transaction, len(blockTxs)) 197 copy(txs, blockTxs) 198 sort.Sort(transactionsByEnergyPrice(txs)) 199 200 var prices []*big.Int 201 for _, tx := range txs { 202 sender, err := types.Sender(signer, tx) 203 if err == nil && sender != block.Coinbase() { 204 prices = append(prices, tx.EnergyPrice()) 205 if len(prices) >= limit { 206 break 207 } 208 } 209 } 210 select { 211 case result <- getBlockPricesResult{prices, nil}: 212 case <-quit: 213 } 214 } 215 216 type bigIntArray []*big.Int 217 218 func (s bigIntArray) Len() int { return len(s) } 219 func (s bigIntArray) Less(i, j int) bool { return s[i].Cmp(s[j]) < 0 } 220 func (s bigIntArray) Swap(i, j int) { s[i], s[j] = s[j], s[i] }