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  }