github.com/codingfuture/orig-energi3@v0.8.4/miner/energi_autocollateral.go (about) 1 // Copyright 2020 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 miner 18 19 import ( 20 "errors" 21 "math/big" 22 "time" 23 24 "github.com/ethereum/go-ethereum/accounts" 25 "github.com/ethereum/go-ethereum/accounts/abi/bind" 26 "github.com/ethereum/go-ethereum/common" 27 "github.com/ethereum/go-ethereum/core/types" 28 "github.com/ethereum/go-ethereum/log" 29 "github.com/ethereum/go-ethereum/params" 30 31 energi_abi "energi.world/core/gen3/energi/abi" 32 energi "energi.world/core/gen3/energi/consensus" 33 energi_params "energi.world/core/gen3/energi/params" 34 ) 35 36 const maxAutoCollateralBlockAge = time.Duration(time.Minute) 37 38 const ( 39 acDisabled uint64 = 0 40 acPostReward uint64 = 1 41 acRapid uint64 = 2 42 ) 43 44 func (w *worker) tryAutocollateral() { 45 w.mu.RLock() 46 defer w.mu.RUnlock() 47 48 if _, ok := w.engine.(*energi.Energi); !ok { 49 // Energi consensus engine not running. 50 log.Debug("energi consensus engine not running") 51 return 52 } 53 54 block := w.eth.BlockChain().CurrentBlock() 55 56 // MN-17 - 2 57 // Check for block timeout. 58 blockTime := time.Unix(int64(block.Time()), 0) 59 timeNow := time.Now().UTC() 60 if timeNow.After(blockTime.Add(maxAutoCollateralBlockAge)) { 61 // if block older is older than maxAutoCollateralBlockAge, exit. 62 log.Debug("block is older than maxAutoCollateralBlockAge") 63 return 64 } 65 66 // Get rewards 67 mnReward, err := w.getBlockReward(energi_params.Energi_MasternodeRegistry, block.Number()) 68 if err != nil { 69 log.Error(err.Error()) 70 return 71 } 72 73 // Skip superblocks 74 // MN-17 - 4 75 if mnReward.Cmp(common.Big0) == 0 { 76 log.Debug("Skipping super block for auto-collateral") 77 return 78 } 79 80 log.Debug("Auto-Collateralize loop") 81 82 for _, wallet := range w.eth.AccountManager().Wallets() { 83 for _, account := range wallet.Accounts() { 84 if wallet.IsUnlockedForStaking(account) { 85 log.Debug("Auto-Collateralize checking", "account", account) 86 87 amount, err := w.hasJustReceivedRewards(account.Address, block, mnReward) 88 if err != nil { 89 log.Debug(err.Error()) 90 if amount == nil || w.autocollateral != acRapid { 91 continue 92 } 93 } 94 95 if _, coins, err := w.doAutocollateral(account.Address, amount); err != nil { 96 // Most likely, an invalid amount to deposit was found in the account. 97 log.Debug("Auto-Collateralize failed", "err", err.Error()) 98 } else { 99 log.Info("Auto-Collateralize successful", "coins deposited", 100 coins.Uint64(), "account", account.Address.String()) 101 } 102 } 103 } 104 } 105 } 106 107 func (w *worker) getBalanceAtBlock(block *types.Block, address common.Address) (*big.Int, error) { 108 stateDb, err := w.eth.BlockChain().StateAt(block.Root()) 109 if err != nil { 110 return nil, err 111 } 112 return stateDb.GetBalance(address), nil 113 } 114 115 // isMNPayouts checks if the provided block in relation to the previous block has 116 // a masternode block payout. 117 func (w *worker) isMNPayouts(currentblock *types.Block, mnOwner common.Address, blockReward *big.Int) (bool, *big.Int, error) { 118 blockNo := currentblock.Number().Uint64() 119 if blockNo <= 0 { 120 return false, nil, errors.New("Invalid block cannot posses MN payouts") 121 } 122 123 balanceNow, err := w.getBalanceAtBlock(currentblock, mnOwner) 124 if err != nil { 125 return false, nil, err 126 } 127 128 // MN-17 - 5 129 // (a).ii Ensure the change was not due to stake reward 130 if currentblock.Coinbase() == mnOwner { 131 return false, balanceNow, errors.New("Stake reward") 132 } 133 134 prevBlock := w.eth.BlockChain().GetBlockByNumber(blockNo - 1) 135 balancePrev, err := w.getBalanceAtBlock(prevBlock, mnOwner) 136 if err != nil { 137 return false, balanceNow, err 138 } 139 140 diff := new(big.Int).Sub(balanceNow, balancePrev) 141 142 // Should be: 0 < diff <= mnPayout 143 status := diff.Cmp(blockReward) <= 0 && diff.Cmp(common.Big0) > 0 144 return status, balanceNow, nil 145 } 146 147 func (w *worker) hasJustReceivedRewards(account common.Address, block *types.Block, mnReward *big.Int) (*big.Int, error) { 148 // MN-17 - 5 149 // (a).i Confirm no MN payouts in the current block. 150 isCurrentMNPayout, balanceNow, err := w.isMNPayouts(block, account, mnReward) 151 if err != nil { 152 return nil, err 153 } 154 155 if isCurrentMNPayout { 156 return balanceNow, errors.New("Current block masternode payout is active") 157 } 158 159 // MN-17 - 5 160 // (a).ii Confirm atleast 1 masternode payout in the previous block. 161 prevBlock := w.eth.BlockChain().GetBlockByNumber(block.Number().Uint64() - 1) 162 isPrevPayout, _, err := w.isMNPayouts(prevBlock, account, mnReward) 163 if err != nil { 164 return balanceNow, err 165 } 166 167 if !isPrevPayout { 168 return balanceNow, errors.New("Expected at least one payout from a previous block") 169 } 170 171 return balanceNow, nil 172 } 173 174 // canAutocollateralize returns the maximum amount that can be deposited as the 175 // collateral if the maximum collateral amount is not yet reached. 176 func (w *worker) canAutocollateralize( 177 account common.Address, 178 amount *big.Int, 179 api *energi_abi.IMasternodeTokenSession, 180 ) (*big.Int, error) { 181 minLimit, maxLimit, err := w.collateralLimits() 182 if err != nil { 183 return nil, err 184 } 185 186 // MN-17 - 5 187 // (b) Ensures that available balance is at least one minimal collateral. 188 if amount.Cmp(minLimit) < 0 { 189 return nil, errors.New("Amount found is less than the minimum required") 190 } 191 192 tokenBalance, err := api.BalanceOf(account) 193 if err != nil { 194 return nil, err 195 } 196 197 // MN-17 - 5 198 // (c) Ensure that the current collateral is below the maximum allowed and more than zero. 199 if tokenBalance.Cmp(common.Big0) <= 0 { 200 return nil, errors.New("No collateral exists") 201 } else if tokenBalance.Cmp(maxLimit) >= 0 { 202 return nil, errors.New("Maximum collateral supported already achieved") 203 } 204 205 modAmount := new(big.Int).Mod(amount, minLimit) 206 amountToDeposit := new(big.Int).Sub(amount, modAmount) 207 208 totalAmount := new(big.Int).Add(tokenBalance, amountToDeposit) 209 if totalAmount.Cmp(maxLimit) == 1 { 210 // Gets the maximum amount to deposit since all the available amount 211 // could breach the max collateral limit if deposited in full. 212 amountToDeposit = new(big.Int).Sub(maxLimit, tokenBalance) 213 } 214 215 return amountToDeposit, nil 216 } 217 218 func (w *worker) doAutocollateral(account common.Address, amount *big.Int) (common.Hash, *big.Int, error) { 219 tokenAPI, err := w.tokenRegistry(account) 220 if err != nil { 221 return common.Hash{}, nil, err 222 } 223 224 // Returns the maximum amount that can be deposited if the collateral max 225 // amount hasn't been reached. 226 newAmount, err := w.canAutocollateralize(account, amount, tokenAPI) 227 if err != nil { 228 return common.Hash{}, nil, err 229 } 230 231 // MN-17 - 5 232 // (d) Perform MNReg.depositCollataral 233 tokenAPI.TransactOpts.Value = newAmount 234 tx, err := tokenAPI.DepositCollateral() 235 if tx == nil || err != nil { 236 return common.Hash{}, nil, err 237 } 238 239 coinsDeposited := new(big.Int).Div(newAmount, big.NewInt(params.Ether)) 240 241 return tx.Hash(), coinsDeposited, nil 242 } 243 244 func (w *worker) getBlockReward(proxy common.Address, blockNumber *big.Int) (*big.Int, error) { 245 contract, err := energi_abi.NewIBlockReward( 246 proxy, w.apiBackend.(bind.ContractBackend)) 247 if err != nil { 248 return nil, err 249 } 250 251 resp, err := contract.GetReward(&bind.CallOpts{}, blockNumber) 252 if err != nil { 253 return nil, err 254 } 255 256 return resp, nil 257 } 258 259 func (w *worker) tokenRegistry(dst common.Address) (*energi_abi.IMasternodeTokenSession, error) { 260 contract, err := energi_abi.NewIMasternodeToken( 261 energi_params.Energi_MasternodeToken, w.apiBackend.(bind.ContractBackend)) 262 if err != nil { 263 return nil, err 264 } 265 session := &energi_abi.IMasternodeTokenSession{ 266 Contract: contract, 267 CallOpts: bind.CallOpts{ 268 Pending: true, 269 From: dst, 270 GasLimit: energi_params.UnlimitedGas, 271 }, 272 TransactOpts: bind.TransactOpts{ 273 From: dst, 274 Signer: w.createStakeTxSignerCallback(), 275 Value: common.Big0, 276 GasLimit: energi_params.MasternodeCallGas, 277 }, 278 } 279 return session, nil 280 } 281 282 func (w *worker) collateralLimits() (minCollateral, maxCollateral *big.Int, err error) { 283 registry, err := energi_abi.NewIMasternodeRegistryV2( 284 energi_params.Energi_MasternodeRegistry, w.apiBackend.(bind.ContractBackend)) 285 if err != nil { 286 return nil, nil, err 287 } 288 289 callOpts := &bind.CallOpts{ 290 GasLimit: energi_params.UnlimitedGas, 291 } 292 293 limits, err := registry.CollateralLimits(callOpts) 294 if err != nil { 295 return nil, nil, err 296 } 297 298 return limits.Min, limits.Max, nil 299 } 300 301 // CreateStakeTxSignerCallback uses unlocked accounts to sign transactions without 302 // a password. 303 func (w *worker) createStakeTxSignerCallback() bind.SignerFn { 304 return func( 305 signer types.Signer, 306 addr common.Address, 307 tx *types.Transaction, 308 ) (*types.Transaction, error) { 309 account := accounts.Account{Address: addr} 310 wallet, err := w.eth.AccountManager().Find(account) 311 if err != nil { 312 return nil, err 313 } 314 315 // MN-17: force transaction creation even when unlocked for staking only 316 h := signer.Hash(tx) 317 sig, err := wallet.SignHash(account, h[:]) 318 if err != nil { 319 return nil, err 320 } 321 322 return tx.WithSignature(signer, sig) 323 } 324 }