diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 3a1d29150f..6a776211a6 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -78,7 +78,10 @@ func newMovingFundsAction( } func (mfa *movingFundsAction) execute() error { - // TODO: Implement + // TODO: Before proceeding with creation of the Bitcoin transaction, wait + // 32 blocks to ensure the commitment transaction has accumulated + // enough confirmations in the Ethereum chain and will not be reverted + // even if a reorg occurs. return nil } diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 4dc2525a0d..5917dc3884 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "math/big" - "sort" "sync" "github.com/keep-network/keep-core/pkg/bitcoin" @@ -708,272 +707,6 @@ func (n *node) handleMovingFundsProposal( walletActionLogger.Infof("wallet action dispatched successfully") } -// TODO: Move the logic from this function into tbtcpg.MovingFundsTask: -func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { - go func() { - logger.Info( - "moving funds proposal initiated for wallet with PKH [0x%x]", - sourceWalletPublicKeyHash, - ) - - // Make sure the wallet meets the criteria for moving funds proposal. - sourceWallet, found := n.walletRegistry.getWalletByPublicKeyHash( - sourceWalletPublicKeyHash, - ) - if !found { - logger.Errorf( - "skipping moving funds proposal for wallet with PKH "+ - "[0x%x] as the node does not control it", - sourceWalletPublicKeyHash, - ) - return - } - - sourceWalletChainData, err := n.chain.GetWallet(sourceWalletPublicKeyHash) - if err != nil { - logger.Errorf( - "failed to get wallet data for source wallet with PKH "+ - "[0x%x]: [%v]", - sourceWalletPublicKeyHash, - err, - ) - return - } - - walletMainUtxo, err := DetermineWalletMainUtxo( - sourceWalletPublicKeyHash, - n.chain, - n.btcChain, - ) - if err != nil { - logger.Errorf( - "skipping moving funds proposal for wallet with PKH "+ - "[0x%x] due to error determining wallet main UTXO: [%v]", - sourceWalletPublicKeyHash, - err, - ) - return - } - - walletBalance := int64(0) - if walletMainUtxo != nil { - walletBalance = walletMainUtxo.Value - } - - if sourceWalletChainData.State != StateMovingFunds { - logger.Infof( - "ignoring moving funds proposal for wallet with PKH [0x%x]; "+ - "wallet not in MovingFunds state", - sourceWalletPublicKeyHash, - ) - return - } - - if sourceWalletChainData.PendingRedemptionsValue > 0 { - logger.Infof( - "ignoring moving funds proposal for wallet with PKH [0x%x]; "+ - "wallet has pending redemptions", - sourceWalletPublicKeyHash, - ) - return - } - - if sourceWalletChainData.PendingMovedFundsSweepRequestsCount > 0 { - logger.Infof( - "ignoring moving funds proposal for wallet with PKH [0x%x]; "+ - "wallet has pending moved funds sweep requests", - sourceWalletPublicKeyHash, - ) - return - } - - if sourceWalletChainData.MovingFundsTargetWalletsCommitmentHash != [32]byte{} { - logger.Infof( - "ignoring moving funds proposal for wallet with PKH [0x%x]; "+ - "wallet has already submitted commitment", - sourceWalletPublicKeyHash, - ) - return - } - - if walletBalance <= 0 { - // The wallet's balance cannot be `0`. Since we are dealing with - // a signed integer we also check it's not negative just in case. - logger.Infof( - "ignoring moving funds proposal for wallet with PKH [0x%x]; "+ - "wallet does not have a positive balance", - sourceWalletPublicKeyHash, - ) - return - } - - logger.Infof( - "proceeding with moving funds commitment for wallet with "+ - "PKH [0x%x]", - sourceWalletPublicKeyHash, - ) - - // Prepare the list of target wallets. - liveWalletsCount, err := n.chain.GetLiveWalletsCount() - if err != nil { - logger.Errorf("failed to get live wallets count: [%v]", err) - return - } - - if liveWalletsCount == 0 { - logger.Infof( - "skipping moving funds proposal for wallet with PKH [0x%x] due"+ - "to lack of live wallets", - sourceWalletPublicKeyHash, - ) - return - } - - _, _, _, _, _, walletMaxBtcTransfer, _, err := n.chain.GetWalletParameters() - if err != nil { - logger.Errorf("failed to get wallet parameters: [%v]", err) - return - } - - if walletMaxBtcTransfer == 0 { - logger.Errorf("wallet max BTC transfer parameter must be positive") - return - } - - ceilingDivide := func(x, y uint64) uint64 { - // The divisor must be positive, but we do not need to check it as - // this function will be executed with wallet max BTC transfer as - // the divisor and we already ensured it is positive. - return (x + y - 1) / y - } - min := func(x, y uint64) uint64 { - if x < y { - return x - } - return y - } - - targetWalletsCount := min( - uint64(liveWalletsCount), - ceilingDivide(uint64(walletBalance), walletMaxBtcTransfer), - ) - - // Prepare a list of target wallets using the new wallets registration - // events. Retrieve only the necessary number of live wallets. - // The iteration is started from the end of the - events, err := n.chain.PastNewWalletRegisteredEvents(nil) - if err != nil { - logger.Errorf( - "failed to get past new wallet registered events: [%v]", - err, - ) - return - } - - targetWallets := make([][20]byte, 0) - - for i := len(events) - 1; i >= 0; i-- { - walletPubKeyHash := events[i].WalletPublicKeyHash - if walletPubKeyHash == sourceWalletPublicKeyHash { - // Just in case make sure not to include the source wallet - // itself. - continue - } - wallet, err := n.chain.GetWallet(walletPubKeyHash) - if err != nil { - logger.Errorf( - "failed to get wallet data for wallet with PKH [0x%x]: [%v]", - walletPubKeyHash, - err, - ) - continue - } - if wallet.State == StateLive { - targetWallets = append(targetWallets, walletPubKeyHash) - } - if len(targetWallets) == int(targetWalletsCount) { - // Stop the iteration if enough live wallets have been gathered. - break - } - } - - if len(targetWallets) != int(targetWalletsCount) { - logger.Errorf( - "failed to get enough target wallets: required [%v]; "+ - "gathered [%v]", - targetWalletsCount, - len(targetWallets), - ) - return - } - - // Sort the target wallets according to their numerical representation - // as the on-chain contract expects. - sort.Slice(targetWallets, func(i, j int) bool { - bigIntI := new(big.Int).SetBytes(targetWallets[i][:]) - bigIntJ := new(big.Int).SetBytes(targetWallets[j][:]) - return bigIntI.Cmp(bigIntJ) < 0 - }) - - logger.Infof("gathered [%v] target wallets", len(targetWallets)) - - walletMemberIDs := make([]uint32, 0) - for _, operatorAddress := range sourceWallet.signingGroupOperators { - operatorID, err := n.getSigningGroupOperatorID(operatorAddress) - if err != nil { - logger.Errorf( - "failed to get operator ID belonging to wallet with "+ - "PKH [0x%x]: [%v]", - sourceWalletPublicKeyHash, - err, - ) - return - } - walletMemberIDs = append(walletMemberIDs, operatorID) - } - - latestBlockHeight, err := n.btcChain.GetLatestBlockHeight() - if err != nil { - logger.Errorf( - "failed to get latest Bitcoin block height: [%v]", - err, - ) - return - } - - // Use the latest Bitcoin block height to determine the wallet member - // index. Increase the result of the modulo operation by one since the - // wallet member index must be within range [1, len(walletMemberIDs)]. - walletMemberIndex := (int(latestBlockHeight) % len(walletMemberIDs)) + 1 - - err = n.chain.SubmitMovingFundsCommitment( - sourceWalletPublicKeyHash, - *walletMainUtxo, - walletMemberIDs, - uint32(walletMemberIndex), - targetWallets, - ) - if err != nil { - logger.Errorf( - "failed to submit moving funds commitment for wallet wit PKH "+ - "[0x%x]: [%v]", - sourceWalletPublicKeyHash, - err, - ) - return - } - - logger.Infof( - "finished moving funds commitment for wallet with PKH [0x%x]", - sourceWalletPublicKeyHash, - ) - - // TODO: Add construction of the move funds Bitcoin transaction. - // Before proceeding with the Bitcoin transaction, check if the - // commitment was successfully submitted. - }() -} - // coordinationLayerSettings represents settings for the coordination layer. type coordinationLayerSettings struct { // executeCoordinationProcedureFn is a function executing the coordination diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index f407ec1f3e..37e2402b5e 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -38,9 +38,69 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( zap.String("walletPKH", fmt.Sprintf("0x%x", walletPublicKeyHash)), ) + liveWalletsCount, err := mft.chain.GetLiveWalletsCount() + if err != nil { + return nil, false, fmt.Errorf( + "cannot get Live wallets count: [%w]", + err, + ) + } + + walletMainUtxo, err := tbtc.DetermineWalletMainUtxo( + walletPublicKeyHash, + mft.chain, + mft.btcChain, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot get wallet's maun UTXO: [%w]", + err, + ) + } + + walletBalance := int64(0) + if walletMainUtxo != nil { + walletBalance = walletMainUtxo.Value + } + + walletChainData, err := mft.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return nil, false, fmt.Errorf( + "cannot get source wallet's chain data: [%w]", + err, + ) + } + + ok, err := mft.CheckEligibility( + taskLogger, + liveWalletsCount, + walletBalance, + walletChainData, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot check wallet eligibility: [%w]", + err, + ) + } + + if !ok { + taskLogger.Info("wallet not eligible for moving funds") + return nil, false, nil + } + + targetWallets, commitmentSubmitted, err := mft.FindTargetWallets( + taskLogger, + walletChainData, + ) + if err != nil { + return nil, false, fmt.Errorf("cannot find target wallets: [%w]", err) + } + proposal, err := mft.ProposeMovingFunds( taskLogger, walletPublicKeyHash, + targetWallets, ) if err != nil { return nil, false, fmt.Errorf( @@ -49,17 +109,79 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( ) } + if !commitmentSubmitted { + mft.SubmitMovingFundsCommitment() + } + return proposal, false, nil } +func (mft *MovingFundsTask) FindTargetWallets( + taskLogger log.StandardLogger, + walletChainData *tbtc.WalletChainData, +) ([][20]byte, bool, error) { + if walletChainData.MovingFundsTargetWalletsCommitmentHash == [32]byte{} { + taskLogger.Infof("Move funds commitment has not been submitted yet") + // TODO: Find the target wallets among Live wallets. + } else { + taskLogger.Infof("Move funds commitment has already been submitted") + // TODO: Find the target wallets from the emitted event. + } + return nil, false, nil +} + +func (mft *MovingFundsTask) CheckEligibility( + taskLogger log.StandardLogger, + liveWalletsCount uint32, + walletBalance int64, + walletChainData *tbtc.WalletChainData, +) (bool, error) { + if liveWalletsCount == 0 { + taskLogger.Infof("there are no Live wallets available") + return false, nil + } + + if walletBalance <= 0 { + // The wallet's balance cannot be `0`. Since we are dealing with + // a signed integer we also check it's not negative just in case. + taskLogger.Infof("source wallet does not have a positive balance") + return false, nil + } + + if walletChainData.State != tbtc.StateMovingFunds { + taskLogger.Infof("source wallet not in MoveFunds state") + return false, nil + } + + if walletChainData.PendingRedemptionsValue > 0 { + taskLogger.Infof("source wallet has pending redemptions") + return false, nil + } + + if walletChainData.PendingMovedFundsSweepRequestsCount > 0 { + taskLogger.Infof("source wallet has pending moved funds sweep requests") + return false, nil + } + + return true, nil +} + +func (mft *MovingFundsTask) SubmitMovingFundsCommitment() ([][20]byte, error) { + // TODO: After submitting the transaction wait until it has at least one + // confirmation. + return nil, nil +} + func (mft *MovingFundsTask) ProposeMovingFunds( taskLogger log.StandardLogger, walletPublicKeyHash [20]byte, + targetWallets [][20]byte, ) (*tbtc.MovingFundsProposal, error) { taskLogger.Infof("preparing a moving funds proposal") proposal := &tbtc.MovingFundsProposal{ WalletPublicKeyHash: walletPublicKeyHash, + TargetWallets: targetWallets, } taskLogger.Infof("validating the moving funds proposal")