github.com/codingfuture/orig-energi3@v0.8.4/core/energi_zerofee.go (about) 1 // Copyright 2019 The Energi Core Authors 2 // This file is part of the Energi Core library. 3 // 4 // The Energi 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 Energi 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 Energi Core library. If not, see <http://www.gnu.org/licenses/>. 16 17 package core 18 19 import ( 20 "errors" 21 "fmt" 22 "math/big" 23 "strings" 24 "time" 25 26 "github.com/ethereum/go-ethereum/accounts/abi" 27 "github.com/ethereum/go-ethereum/common" 28 "github.com/ethereum/go-ethereum/core/types" 29 "github.com/ethereum/go-ethereum/core/vm" 30 "github.com/ethereum/go-ethereum/log" 31 32 energi_abi "energi.world/core/gen3/energi/abi" 33 energi_params "energi.world/core/gen3/energi/params" 34 ) 35 36 const ( 37 ZeroFeeGasLimit uint64 = 500000 38 ) 39 40 var ( 41 energiClaimID types.MethodID 42 energiVerifyClaimID types.MethodID 43 energiMNHeartbeatID types.MethodID 44 energiMNInvalidateID types.MethodID 45 energiCPSignID types.MethodID 46 ) 47 48 func init() { 49 migration_abi, err := abi.JSON(strings.NewReader(energi_abi.Gen2MigrationABI)) 50 if err != nil { 51 panic(err) 52 } 53 54 copy(energiClaimID[:], migration_abi.Methods["claim"].Id()) 55 copy(energiVerifyClaimID[:], migration_abi.Methods["verifyClaim"].Id()) 56 57 mnreg_abi, err := abi.JSON(strings.NewReader(energi_abi.IMasternodeRegistryV2ABI)) 58 if err != nil { 59 panic(err) 60 } 61 copy(energiMNHeartbeatID[:], mnreg_abi.Methods["heartbeat"].Id()) 62 copy(energiMNInvalidateID[:], mnreg_abi.Methods["invalidate"].Id()) 63 64 cpreg_abi, err := abi.JSON(strings.NewReader(energi_abi.ICheckpointRegistryABI)) 65 if err != nil { 66 panic(err) 67 } 68 copy(energiCPSignID[:], cpreg_abi.Methods["sign"].Id()) 69 } 70 71 /** 72 * SC-7: Zero-fee transactions 73 * 74 * Check if Energi consensus allows the transaction to be processed as 75 * zero-fee. 76 */ 77 func IsValidZeroFee(tx *types.Transaction) bool { 78 // Skip check for non-zero price 79 if tx.Cost().Cmp(common.Big0) != 0 { 80 return false 81 } 82 83 if tx.Gas() > ZeroFeeGasLimit { 84 log.Trace("Zero-fee gas is over limit", "hash", tx.Hash(), "limit", tx.Gas()) 85 return false 86 } 87 88 if IsGen2Migration(tx) { 89 return true 90 } 91 92 if IsMasternodeCall(tx) { 93 return true 94 } 95 96 if IsCheckpointCall(tx) { 97 return true 98 } 99 100 return false 101 } 102 103 func IsGen2Migration(tx *types.Transaction) bool { 104 to := tx.To() 105 106 return (to != nil) && 107 (*to == energi_params.Energi_MigrationContract) && 108 (tx.MethodID() == energiClaimID) 109 } 110 111 func IsMasternodeCall(tx *types.Transaction) bool { 112 to := tx.To() 113 114 if (to == nil) || (*to != energi_params.Energi_MasternodeRegistry) { 115 return false 116 } 117 118 if method := tx.MethodID(); method == energiMNHeartbeatID { 119 return true 120 } else if method == energiMNInvalidateID { 121 return true 122 } 123 124 return false 125 } 126 127 func IsCheckpointCall(tx *types.Transaction) bool { 128 to := tx.To() 129 130 if (to == nil) || (*to != energi_params.Energi_CheckpointRegistry) { 131 return false 132 } 133 134 return tx.MethodID() == energiCPSignID 135 } 136 137 func IsBlacklisted(db vm.StateDB, addr common.Address) bool { 138 return db.GetState(energi_params.Energi_Blacklist, addr.Hash()) != common.Hash{} 139 } 140 141 func IsWhitelisted(db vm.StateDB, addr common.Address) bool { 142 return db.GetState(energi_params.Energi_Whitelist, addr.Hash()) != common.Hash{} 143 } 144 145 //============================================================================= 146 147 var ( 148 zfCleanupTimeout = time.Minute 149 zfMinHeartbeatPeriod = time.Duration(30) * time.Minute 150 zfMinInvalidationPeriod = time.Duration(2) * time.Minute 151 zfMinCoinClaimPeriod = time.Duration(3) * time.Minute 152 zfMinCheckpointPeriod = time.Duration(10) * time.Minute 153 154 ErrZeroFeeDoS = errors.New("zero-fee DoS") 155 ) 156 157 type zeroFeeProtector struct { 158 mnHeartbeats map[common.Address]time.Time 159 mnInvalidations map[common.Address]time.Time 160 mnCheckpoints map[common.Address]time.Time 161 coinClaims map[uint32]time.Time 162 nextCleanup time.Time 163 timeNow func() time.Time 164 } 165 166 func newZeroFeeProtector() *zeroFeeProtector { 167 return &zeroFeeProtector{ 168 mnHeartbeats: make(map[common.Address]time.Time), 169 mnInvalidations: make(map[common.Address]time.Time), 170 mnCheckpoints: make(map[common.Address]time.Time), 171 coinClaims: make(map[uint32]time.Time), 172 nextCleanup: time.Now().Add(zfCleanupTimeout), 173 timeNow: time.Now, 174 } 175 } 176 177 func (z *zeroFeeProtector) cleanupTimeout( 178 now time.Time, 179 timeMap map[common.Address]time.Time, 180 timeout time.Duration, 181 ) { 182 for k, v := range timeMap { 183 if now.Sub(v) > timeout { 184 delete(timeMap, k) 185 } 186 } 187 } 188 189 func (z *zeroFeeProtector) cleanupBySender( 190 sender common.Address, 191 timeMap map[common.Address]time.Time, 192 ) { 193 if _, ok := timeMap[sender]; ok { 194 delete(timeMap, sender) 195 } 196 } 197 198 func (z *zeroFeeProtector) cleanupAllBySender(sender common.Address) { 199 z.cleanupBySender(sender, z.mnHeartbeats) 200 z.cleanupBySender(sender, z.mnInvalidations) 201 z.cleanupBySender(sender, z.mnCheckpoints) 202 } 203 204 func (z *zeroFeeProtector) cleanupAllByTimeout(now time.Time) { 205 if z.nextCleanup.After(now) { 206 return 207 } 208 209 z.nextCleanup = now.Add(zfCleanupTimeout) 210 //--- 211 212 z.cleanupTimeout(now, z.mnHeartbeats, zfMinHeartbeatPeriod) 213 z.cleanupTimeout(now, z.mnInvalidations, zfMinInvalidationPeriod) 214 z.cleanupTimeout(now, z.mnCheckpoints, zfMinCheckpointPeriod) 215 216 for k, v := range z.coinClaims { 217 if now.Sub(v) > zfMinCoinClaimPeriod { 218 delete(z.coinClaims, k) 219 } 220 } 221 } 222 223 func (z *zeroFeeProtector) checkMasternode( 224 pool *TxPool, 225 sender common.Address, 226 now time.Time, 227 timeMap map[common.Address]time.Time, 228 timeout time.Duration, 229 ) error { 230 if v, ok := timeMap[sender]; ok && now.Sub(v) < timeout { 231 log.Debug("ZeroFee DoS by time", "sender", sender, "interval", now.Sub(v)) 232 return ErrZeroFeeDoS 233 } 234 235 // NOTE: potential issue with nonce gap 236 mn_indicator := pool.currentState.GetState( 237 energi_params.Energi_MasternodeList, sender.Hash()) 238 if (mn_indicator == common.Hash{}) { 239 log.Debug("ZeroFee DoS by inactive MN", "sender", sender) 240 return ErrZeroFeeDoS 241 } 242 243 timeMap[sender] = now 244 log.Debug("ZeroFee masternode", "sender", sender, "now", now) 245 return nil 246 } 247 248 func (z *zeroFeeProtector) checkMigration( 249 pool *TxPool, 250 sender common.Address, 251 now time.Time, 252 tx *types.Transaction, 253 ) error { 254 callData := tx.Data() 255 if len(callData) <= 36 { 256 log.Debug("Missing tx data") 257 return fmt.Errorf("invalid tx: missing tx data length") 258 } 259 260 item_id := uint32(new(big.Int).SetBytes(callData[4:36]).Uint64()) 261 262 if v, ok := z.coinClaims[item_id]; ok && now.Sub(v) < zfMinCoinClaimPeriod { 263 log.Debug("ZeroFee DoS by time", "item_id", item_id, "interval", now.Sub(v)) 264 return ErrZeroFeeDoS 265 } 266 267 // Check if call is valid 268 //--- 269 copy(callData[:], energiVerifyClaimID[:]) 270 271 msg := types.NewMessage( 272 sender, 273 tx.To(), 274 tx.Nonce(), 275 tx.Value(), 276 tx.Gas(), 277 tx.GasPrice(), 278 callData, 279 false, 280 ) 281 282 // Just in case: safety measure 283 statedb := pool.currentState.Copy() 284 285 bc, ok := pool.chain.(*BlockChain) 286 if bc == nil || !ok { 287 log.Debug("ZeroFee DoS on missing blockchain") 288 return ErrZeroFeeDoS 289 } 290 vmc := bc.GetVMConfig() 291 ctx := NewEVMContext(msg, bc.CurrentHeader(), bc, &sender) 292 ctx.GasLimit = ZeroFeeGasLimit 293 evm := vm.NewEVM(ctx, statedb, bc.Config(), *vmc) 294 295 gp := new(GasPool).AddGas(tx.Gas()) 296 output, _, failed, err := ApplyMessage(evm, msg, gp) 297 if failed || err != nil { 298 log.Debug("ZeroFee DoS by execution", 299 "item", item_id, "err", err, "output", output) 300 return ErrZeroFeeDoS 301 } 302 303 if len(output) != len(common.Hash{}) { 304 log.Debug("ZeroFee DoS by unpack", "item", item_id, "output", output) 305 return ErrZeroFeeDoS 306 } 307 308 amount := new(big.Int).SetBytes(output) 309 310 if amount.Cmp(common.Big0) <= 0 { 311 log.Debug("ZeroFee DoS by already claimed", "item", item_id) 312 return ErrZeroFeeDoS 313 } 314 315 //--- 316 z.coinClaims[item_id] = now 317 log.Debug("ZeroFee migration", "item_id", item_id, "now", now) 318 return nil 319 } 320 321 func (z *zeroFeeProtector) checkDoS(pool *TxPool, tx *types.Transaction) error { 322 now := z.timeNow() 323 324 defer z.cleanupAllByTimeout(now) 325 326 sender, err := types.Sender(pool.signer, tx) 327 if err != nil { 328 log.Debug("ZeroFee DoS sender error", "err", err) 329 return err 330 } 331 332 // NOTE: assumed to be called only on zero fee 333 if method := tx.MethodID(); method == energiMNHeartbeatID { 334 return z.checkMasternode(pool, sender, now, z.mnHeartbeats, zfMinHeartbeatPeriod) 335 } else if method == energiMNInvalidateID { 336 return z.checkMasternode(pool, sender, now, z.mnInvalidations, zfMinInvalidationPeriod) 337 } else if method == energiClaimID { 338 return z.checkMigration(pool, sender, now, tx) 339 } else if method == energiCPSignID { 340 return z.checkMasternode(pool, sender, now, z.mnCheckpoints, zfMinCheckpointPeriod) 341 } 342 return nil 343 }