diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 6d35720cbc..acc94b84f7 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1705,6 +1705,7 @@ func (tc *TbtcChain) ValidateHeartbeatProposal( } func (tc *TbtcChain) ValidateMovingFundsProposal( + walletPublicKeyHash [20]byte, proposal *tbtc.MovingFundsProposal, ) error { // TODO: Implement diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 8083bebe9b..63ee271e77 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -188,22 +188,6 @@ type BridgeChain interface { // if the wallet was not found. GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error) - // GetWalletParameters gets the current value of parameters relevant to - // wallet. - GetWalletParameters() ( - creationPeriod uint32, - creationMinBtcBalance uint64, - creationMaxBtcBalance uint64, - closureMinBtcBalance uint64, - maxAge uint32, - maxBtcTransfer uint64, - closingPeriod uint32, - err error, - ) - - // GetLiveWalletsCount gets the current count of live wallets. - GetLiveWalletsCount() (uint32, error) - // ComputeMainUtxoHash computes the hash of the provided main UTXO // according to the on-chain Bridge rules. ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte @@ -238,14 +222,6 @@ type BridgeChain interface { fundingOutputIndex uint32, ) (*DepositChainRequest, bool, error) - // PastNewWalletRegisteredEvents fetches past new wallet registered events - // according to the provided filter or unfiltered if the filter is nil. Returned - // events are sorted by the block number in the ascending order, i.e. the - // latest event is at the end of the slice. - PastNewWalletRegisteredEvents( - filter *NewWalletRegisteredEventFilter, - ) ([]*NewWalletRegisteredEvent, error) - // Submits the moving funds target wallets commitment. SubmitMovingFundsCommitment( walletPublicKeyHash [20]byte, @@ -386,7 +362,10 @@ type WalletProposalValidatorChain interface { // ValidateMovingFundsProposal validates the given moving funds proposal // against the chain. Returns an error if the proposal is not valid or // nil otherwise. - ValidateMovingFundsProposal(proposal *MovingFundsProposal) error + ValidateMovingFundsProposal( + walletPublicKeyHash [20]byte, + proposal *MovingFundsProposal, + ) error } // RedemptionRequestedEvent represents a redemption requested event. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 4d1ea4c36b..363991e10f 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -821,13 +821,6 @@ func (lc *localChain) ValidateRedemptionProposal( return nil } -func (lc *localChain) ValidateMovingFundsProposal( - proposal *MovingFundsProposal, -) error { - // TODO: Implement - return nil -} - func (lc *localChain) setRedemptionProposalValidationResult( walletPublicKeyHash [20]byte, proposal *RedemptionProposal, @@ -895,6 +888,14 @@ func (lc *localChain) setHeartbeatProposalValidationResult( lc.heartbeatProposalValidations[proposal.Message] = result } +func (lc *localChain) ValidateMovingFundsProposal( + walletPublicKeyHash [20]byte, + proposal *MovingFundsProposal, +) error { + // TODO: Implement + panic("unsupported") +} + // Connect sets up the local chain. func Connect(blockTime ...time.Duration) *localChain { operatorPrivateKey, _, err := operator.GenerateKeyPair(local_v1.DefaultCurve) diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 976648b079..749b91ff5f 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -23,6 +23,22 @@ type Chain interface { filter *tbtc.NewWalletRegisteredEventFilter, ) ([]*tbtc.NewWalletRegisteredEvent, error) + // GetWalletParameters gets the current value of parameters relevant to + // wallet. + GetWalletParameters() ( + creationPeriod uint32, + creationMinBtcBalance uint64, + creationMaxBtcBalance uint64, + closureMinBtcBalance uint64, + maxAge uint32, + maxBtcTransfer uint64, + closingPeriod uint32, + err error, + ) + + // GetLiveWalletsCount gets the current count of live wallets. + GetLiveWalletsCount() (uint32, error) + // BuildDepositKey calculates a deposit key for the given funding transaction // which is a unique identifier for a deposit on-chain. BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int @@ -111,4 +127,12 @@ type Chain interface { walletPublicKeyHash [20]byte, proposal *tbtc.HeartbeatProposal, ) error + + // ValidateMovingFundsProposal validates the given moving funds proposal + // against the chain. Returns an error if the proposal is not valid or + // nil otherwise. + ValidateMovingFundsProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.MovingFundsProposal, + ) error } diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index fc7df46ed5..0648009031 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -616,6 +616,22 @@ func (lc *LocalChain) SetHeartbeatProposalValidationResult( lc.heartbeatProposalValidations[proposal.Message] = result } +func (lc *LocalChain) ValidateMovingFundsProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.MovingFundsProposal, +) error { + // TODO: Implement + panic("unsupported") +} + +func (lc *LocalChain) SetMovingFundsProposalValidationResult( + proposal *tbtc.MovingFundsProposal, + result bool, +) { + // TODO: Implement + panic("unsupported") +} + func buildRedemptionProposalValidationKey( walletPublicKeyHash [20]byte, proposal *tbtc.RedemptionProposal, diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 880fef7064..5aaf02d535 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -2,6 +2,8 @@ package tbtcpg import ( "fmt" + "math/big" + "sort" "go.uber.org/zap" @@ -38,14 +40,30 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( zap.String("walletPKH", fmt.Sprintf("0x%x", walletPublicKeyHash)), ) - liveWalletsCount, err := mft.chain.GetLiveWalletsCount() + // Check if the wallet is eligible for moving funds. + walletChainData, err := mft.chain.GetWallet(walletPublicKeyHash) if err != nil { return nil, false, fmt.Errorf( - "cannot get Live wallets count: [%w]", + "cannot get source wallet's chain data: [%w]", err, ) } + if walletChainData.State != tbtc.StateMovingFunds { + taskLogger.Infof("source wallet not in MoveFunds state") + return nil, false, nil + } + + if walletChainData.PendingRedemptionsValue > 0 { + taskLogger.Infof("source wallet has pending redemptions") + return nil, false, nil + } + + if walletChainData.PendingMovedFundsSweepRequestsCount > 0 { + taskLogger.Infof("source wallet has pending moved funds sweep requests") + return nil, false, nil + } + walletMainUtxo, err := tbtc.DetermineWalletMainUtxo( walletPublicKeyHash, mft.chain, @@ -63,35 +81,32 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( 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, - ) + 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 nil, false, nil } - ok, err := mft.CheckEligibility( - taskLogger, - liveWalletsCount, - walletBalance, - walletChainData, - ) + liveWalletsCount, err := mft.chain.GetLiveWalletsCount() if err != nil { return nil, false, fmt.Errorf( - "cannot check wallet eligibility: [%w]", + "cannot get Live wallets count: [%w]", err, ) } - if !ok { - taskLogger.Info("wallet not eligible for moving funds") + if liveWalletsCount == 0 { + taskLogger.Infof("there are no Live wallets available") return nil, false, nil } targetWallets, commitmentSubmitted, err := mft.FindTargetWallets( taskLogger, + walletPublicKeyHash, walletChainData, + uint64(walletBalance), + liveWalletsCount, ) if err != nil { return nil, false, fmt.Errorf("cannot find target wallets: [%w]", err) @@ -117,52 +132,133 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( func (mft *MovingFundsTask) FindTargetWallets( taskLogger log.StandardLogger, + sourceWalletPublicKeyHash [20]byte, walletChainData *tbtc.WalletChainData, + walletBalance uint64, + liveWalletsCount uint32, ) ([][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 mft.findNewTargetWallets( + taskLogger, + sourceWalletPublicKeyHash, + walletBalance, + liveWalletsCount, + ) } - return nil, false, nil + + return mft.retrieveCommittedTargetWallets(taskLogger) } -func (mft *MovingFundsTask) CheckEligibility( +func (mft *MovingFundsTask) findNewTargetWallets( taskLogger log.StandardLogger, + sourceWalletPublicKeyHash [20]byte, + walletBalance uint64, liveWalletsCount uint32, - walletBalance int64, - walletChainData *tbtc.WalletChainData, -) (bool, error) { - if liveWalletsCount == 0 { - taskLogger.Infof("there are no Live wallets available") - return false, nil +) ([][20]byte, bool, error) { + taskLogger.Infof( + "commitment not submitted yet; looking for new target wallets", + ) + + _, _, _, _, _, walletMaxBtcTransfer, _, err := mft.chain.GetWalletParameters() + if err != nil { + return nil, false, fmt.Errorf("cannot get wallet parameters: [%w]", err) } - 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 walletMaxBtcTransfer == 0 { + return nil, false, fmt.Errorf( + "wallet max BTC transfer must be positive: [%w]", err, + ) } - if walletChainData.State != tbtc.StateMovingFunds { - taskLogger.Infof("source wallet not in MoveFunds state") - return false, nil + 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 } - if walletChainData.PendingRedemptionsValue > 0 { - taskLogger.Infof("source wallet has pending redemptions") - return false, nil + targetWalletsCount := min( + uint64(liveWalletsCount), + ceilingDivide(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 list as the newest wallets + // are located there and have highest the chance of being Live. + events, err := mft.chain.PastNewWalletRegisteredEvents(nil) + if err != nil { + return nil, false, fmt.Errorf( + "failed to get past new wallet registered events: [%v]", + err, + ) } - if walletChainData.PendingMovedFundsSweepRequestsCount > 0 { - taskLogger.Infof("source wallet has pending moved funds sweep requests") - return false, nil + 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 := mft.chain.GetWallet(walletPubKeyHash) + if err != nil { + taskLogger.Errorf( + "failed to get wallet data for wallet with PKH [0x%x]: [%v]", + walletPubKeyHash, + err, + ) + continue + } + + if wallet.State == tbtc.StateLive { + targetWallets = append(targetWallets, walletPubKeyHash) + } + if len(targetWallets) == int(targetWalletsCount) { + // Stop the iteration if enough live wallets have been gathered. + break + } } - return true, nil + if len(targetWallets) != int(targetWalletsCount) { + return nil, false, fmt.Errorf( + "failed to get enough target wallets: required [%v]; gathered [%v]", + targetWalletsCount, + len(targetWallets), + ) + } + + // 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)) + + return targetWallets, false, nil +} + +func (mft *MovingFundsTask) retrieveCommittedTargetWallets( + taskLogger log.StandardLogger, +) ([][20]byte, bool, error) { + taskLogger.Infof( + "commitment already submitted; retrieving committed target wallets", + ) + + // TODO: Implement + return nil, false, nil } func (mft *MovingFundsTask) SubmitMovingFundsCommitment() ([][20]byte, error) {