From 6ff4af7f1a7b2181dbb57f5248650d94bfcb0490 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 30 Nov 2023 18:01:44 +0100 Subject: [PATCH 01/50] Added moving funds commitment implementation --- pkg/chain/ethereum/tbtc.go | 53 +++++++++ pkg/tbtc/chain.go | 33 ++++++ pkg/tbtc/chain_test.go | 33 ++++++ pkg/tbtc/moving_funds.go | 5 + pkg/tbtc/node.go | 219 ++++++++++++++++++++++++++++++++++++- 5 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 pkg/tbtc/moving_funds.go diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index e706320621..ea2c1f0ff7 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1711,6 +1711,37 @@ func (tc *TbtcChain) GetWalletLock( return time.Unix(int64(lock.ExpiresAt), 0), cause, nil } +func (tc *TbtcChain) GetWalletParameters() ( + creationPeriod uint32, + creationMinBtcBalance uint64, + creationMaxBtcBalance uint64, + closureMinBtcBalance uint64, + maxAge uint32, + maxBtcTransfer uint64, + closingPeriod uint32, + err error, +) { + parameters, callErr := tc.bridge.WalletParameters() + if callErr != nil { + err = callErr + return + } + + creationPeriod = parameters.WalletCreationPeriod + creationMinBtcBalance = parameters.WalletCreationMinBtcBalance + creationMaxBtcBalance = parameters.WalletCreationMaxBtcBalance + closureMinBtcBalance = parameters.WalletClosureMinBtcBalance + maxAge = parameters.WalletMaxAge + maxBtcTransfer = parameters.WalletMaxBtcTransfer + closingPeriod = parameters.WalletClosingPeriod + + return +} + +func (tc *TbtcChain) GetLiveWalletsCount() (uint32, error) { + return tc.bridge.LiveWalletsCount() +} + func parseWalletActionType(value uint8) (tbtc.WalletActionType, error) { switch value { case 0: @@ -1824,6 +1855,28 @@ func (tc *TbtcChain) OnRedemptionProposalSubmitted( OnEvent(onEvent) } +func (tc *TbtcChain) SubmitMovingFundsCommitment( + walletPublicKeyHash [20]byte, + walletMainUTXO bitcoin.UnspentTransactionOutput, + walletMembersIDs []uint32, + walletMemberIndex uint32, + targetWallets [][20]byte, +) error { + mainUtxo := tbtcabi.BitcoinTxUTXO{ + TxHash: walletMainUTXO.Outpoint.TransactionHash, + TxOutputIndex: walletMainUTXO.Outpoint.OutputIndex, + TxOutputValue: uint64(walletMainUTXO.Value), + } + _, err := tc.bridge.SubmitMovingFundsCommitment( + walletPublicKeyHash, + mainUtxo, + walletMembersIDs, + big.NewInt(int64(walletMemberIndex)), + targetWallets, + ) + return err +} + func (tc *TbtcChain) ValidateRedemptionProposal( proposal *tbtc.RedemptionProposal, ) error { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index fe2240b0c4..486a857379 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -188,6 +188,22 @@ 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 @@ -221,6 +237,14 @@ type BridgeChain interface { fundingTxHash bitcoin.Hash, 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) } // NewWalletRegisteredEvent represents a new wallet registered event. @@ -343,6 +367,15 @@ type WalletCoordinatorChain interface { func(event *RedemptionProposalSubmittedEvent), ) subscription.EventSubscription + // Submits the moving funds target wallets commitment. + SubmitMovingFundsCommitment( + walletPublicKeyHash [20]byte, + walletMainUTXO bitcoin.UnspentTransactionOutput, + walletMembersIDs []uint32, + walletMemberIndex uint32, + targetWallets [][20]byte, + ) error + // ValidateDepositSweepProposal validates the given deposit sweep proposal // against the chain. It requires some additional data about the deposits // that must be fetched externally. Returns an error if the proposal is diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index eaad15c787..db8aca5e7e 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -586,6 +586,12 @@ func (lc *localChain) GetDepositRequest( panic("not supported") } +func (lc *localChain) PastNewWalletRegisteredEvents( + filter *NewWalletRegisteredEventFilter, +) ([]*NewWalletRegisteredEvent, error) { + panic("not supported") +} + func (lc *localChain) setPendingRedemptionRequest( walletPublicKeyHash [20]byte, request *RedemptionRequest, @@ -683,6 +689,23 @@ func (lc *localChain) GetWalletLock(walletPublicKeyHash [20]byte) ( panic("unsupported") } +func (lc *localChain) GetWalletParameters() ( + creationPeriod uint32, + creationMinBtcBalance uint64, + creationMaxBtcBalance uint64, + closureMinBtcBalance uint64, + maxAge uint32, + maxBtcTransfer uint64, + closingPeriod uint32, + err error, +) { + panic("unsupported") +} + +func (lc *localChain) GetLiveWalletsCount() (uint32, error) { + panic("unsupported") +} + func (lc *localChain) ValidateDepositSweepProposal( proposal *DepositSweepProposal, depositsExtraInfo []struct { @@ -773,6 +796,16 @@ func (lc *localChain) OnRedemptionProposalSubmitted( panic("unsupported") } +func (lc *localChain) SubmitMovingFundsCommitment( + walletPublicKeyHash [20]byte, + walletMainUTXO bitcoin.UnspentTransactionOutput, + walletMembersIDs []uint32, + walletMemberIndex uint32, + targetWallets [][20]byte, +) error { + panic("unsupported") +} + func (lc *localChain) ValidateRedemptionProposal( proposal *RedemptionProposal, ) error { diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go new file mode 100644 index 0000000000..7793af694e --- /dev/null +++ b/pkg/tbtc/moving_funds.go @@ -0,0 +1,5 @@ +package tbtc + +type MovingFundsProposal struct { + WalletPublicKeyHash [20]byte +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index b4e167a5b4..b6863f506c 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -6,11 +6,11 @@ import ( "encoding/hex" "fmt" "math/big" + "sort" "sync" "time" "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/chain" "go.uber.org/zap" @@ -668,6 +668,223 @@ func (n *node) handleRedemptionProposal( walletActionLogger.Infof("wallet action dispatched successfully") } +func (n *node) handleMovingFundsProposal(proposal *MovingFundsProposal) { + go func() { + sourceWalletPublicKeyHash := proposal.WalletPublicKeyHash + logger.Info( + "moving funds proposal initiated for wallet with PKH [0x%x]", + sourceWalletPublicKeyHash, + ) + + // Make sure the wallet meets the criteria for moving funds proposal. + walletRegistryData, 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 := walletMainUtxo.Value + + // Check if the wallet meets the conditions for moving funds + // commitment. + if sourceWalletChainData.State != StateMovingFunds || + sourceWalletChainData.PendingRedemptionsValue > 0 || + sourceWalletChainData.PendingMovedFundsSweepRequestsCount > 0 || + sourceWalletChainData.MovingFundsTargetWalletsCommitmentHash != [32]byte{} || + walletBalance <= 0 { + logger.Errorf( + "skipping moving funds proposal for wallet with PKH "+ + "[0x%x] due to unmet conditions", + sourceWalletPublicKeyHash, + ) + return + } + + logger.Infof( + "proceeding with moving funds commitment for wallet with "+ + "PKH [0x%x]", + sourceWalletPublicKeyHash, + ) + + // Retrieve all the live 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 + } + + events, err := n.chain.PastNewWalletRegisteredEvents(nil) + if err != nil { + logger.Errorf( + "failed to get past new wallet registered events: [%v]", + err, + ) + return + } + + liveWallets := make([][20]byte, 0) + + for _, event := range events { + walletPubKeyHash := event.WalletPublicKeyHash + 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 { + liveWallets = append(liveWallets, walletPubKeyHash) + } + } + + // Make sure all the live wallets data has been retrieved by + // comparing the number of live wallets to the on-chain counter. + if len(liveWallets) != int(liveWalletsCount) { + logger.Errorf( + "mismatch between the number of retrieved live wallets "+ + "and the on-chain live wallet count [%v:%v]", + len(liveWallets), + int(liveWalletsCount), + ) + return + } + + // Sort the live wallets according to their numerical representation + // as the on-chain contract expects. + sort.Slice(liveWallets, func(i, j int) bool { + bigIntI := new(big.Int).SetBytes(liveWallets[i][:]) + bigIntJ := new(big.Int).SetBytes(liveWallets[j][:]) + return bigIntI.Cmp(bigIntJ) < 0 + }) + + logger.Infof("found [%v] live wallets", len(liveWallets)) + + _, _, _, _, _, walletMaxBtcTransfer, _, err := n.chain.GetWalletParameters() + if err != nil { + logger.Errorf("failed to get wallet parameters: [%v]", err) + return + } + + ceilingDivide := func(x, y uint64) uint64 { + 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), + ) + + targetWallets := liveWallets[0:targetWalletsCount] + logger.Infof("Target wallets length [%v]", len(targetWallets)) + + walletMemberIDs := make([]uint32, 0) + for _, operatorAddress := range walletRegistryData.signingGroupOperators { + operatorId, err := n.chain.GetOperatorID(operatorAddress) + if err != nil { + logger.Errorf( + "failed to get operator ID for operator [%v] belonging to "+ + "wallet with PKH [0x%x]: [%v]", + operatorAddress, + 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 From 76ac5736cf8b460e55218ddf34e29dbbfb83d83b Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 1 Dec 2023 16:16:45 +0100 Subject: [PATCH 02/50] Renamed variable --- pkg/tbtc/node.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index b6863f506c..19dd9ad9bb 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -677,7 +677,7 @@ func (n *node) handleMovingFundsProposal(proposal *MovingFundsProposal) { ) // Make sure the wallet meets the criteria for moving funds proposal. - walletRegistryData, found := n.walletRegistry.getWalletByPublicKeyHash( + sourceWallet, found := n.walletRegistry.getWalletByPublicKeyHash( sourceWalletPublicKeyHash, ) if !found { @@ -828,7 +828,7 @@ func (n *node) handleMovingFundsProposal(proposal *MovingFundsProposal) { logger.Infof("Target wallets length [%v]", len(targetWallets)) walletMemberIDs := make([]uint32, 0) - for _, operatorAddress := range walletRegistryData.signingGroupOperators { + for _, operatorAddress := range sourceWallet.signingGroupOperators { operatorId, err := n.chain.GetOperatorID(operatorAddress) if err != nil { logger.Errorf( From 8a078f2d927e0d81f1f26c33c5127f2885d6774c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 1 Dec 2023 16:20:44 +0100 Subject: [PATCH 03/50] Added check for nil wallet's main UTXO --- pkg/tbtc/node.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 19dd9ad9bb..cfa7ce99c3 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -715,7 +715,10 @@ func (n *node) handleMovingFundsProposal(proposal *MovingFundsProposal) { return } - walletBalance := walletMainUtxo.Value + walletBalance := int64(0) + if walletMainUtxo != nil { + walletBalance = walletMainUtxo.Value + } // Check if the wallet meets the conditions for moving funds // commitment. From ae886cd4ea7bb91ea9045d967b99f7e9de2e6228 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 1 Dec 2023 16:26:08 +0100 Subject: [PATCH 04/50] Used wallet public key hash as function's argument --- pkg/tbtc/moving_funds.go | 5 ----- pkg/tbtc/node.go | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 pkg/tbtc/moving_funds.go diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go deleted file mode 100644 index 7793af694e..0000000000 --- a/pkg/tbtc/moving_funds.go +++ /dev/null @@ -1,5 +0,0 @@ -package tbtc - -type MovingFundsProposal struct { - WalletPublicKeyHash [20]byte -} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index cfa7ce99c3..79f514651a 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -668,9 +668,8 @@ func (n *node) handleRedemptionProposal( walletActionLogger.Infof("wallet action dispatched successfully") } -func (n *node) handleMovingFundsProposal(proposal *MovingFundsProposal) { +func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { go func() { - sourceWalletPublicKeyHash := proposal.WalletPublicKeyHash logger.Info( "moving funds proposal initiated for wallet with PKH [0x%x]", sourceWalletPublicKeyHash, From 2d07451604a978f2ef708ff3615648b7267c0314 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 4 Dec 2023 14:26:20 +0100 Subject: [PATCH 05/50] Split condition check --- pkg/tbtc/node.go | 50 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 79f514651a..7cdb5d1ffb 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -719,16 +719,46 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { walletBalance = walletMainUtxo.Value } - // Check if the wallet meets the conditions for moving funds - // commitment. - if sourceWalletChainData.State != StateMovingFunds || - sourceWalletChainData.PendingRedemptionsValue > 0 || - sourceWalletChainData.PendingMovedFundsSweepRequestsCount > 0 || - sourceWalletChainData.MovingFundsTargetWalletsCommitmentHash != [32]byte{} || - walletBalance <= 0 { - logger.Errorf( - "skipping moving funds proposal for wallet with PKH "+ - "[0x%x] due to unmet conditions", + 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 { + logger.Infof( + "ignoring moving funds proposal for wallet with PKH [0x%x]; "+ + "wallet does not have a positive balance", sourceWalletPublicKeyHash, ) return From 434f05710995236ae749e9fd1575e4d1a3ed3deb Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 4 Dec 2023 14:31:10 +0100 Subject: [PATCH 06/50] Added additional target wallet check --- pkg/tbtc/node.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 7cdb5d1ffb..e4ca3cf3e0 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -799,6 +799,9 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { for _, event := range events { walletPubKeyHash := event.WalletPublicKeyHash + if walletPubKeyHash == sourceWalletPublicKeyHash { + continue + } wallet, err := n.chain.GetWallet(walletPubKeyHash) if err != nil { logger.Errorf( From 377050e7ec89c951fbab5f99440c6de23b66089c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 4 Dec 2023 14:35:58 +0100 Subject: [PATCH 07/50] Made log messages begin with lower case --- pkg/tbtc/node.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index e4ca3cf3e0..c67909e128 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -860,7 +860,7 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { ) targetWallets := liveWallets[0:targetWalletsCount] - logger.Infof("Target wallets length [%v]", len(targetWallets)) + logger.Infof("target wallets length [%v]", len(targetWallets)) walletMemberIDs := make([]uint32, 0) for _, operatorAddress := range sourceWallet.signingGroupOperators { @@ -910,7 +910,7 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { } logger.Infof( - "Finished moving funds commitment for wallet with PKH [0x%x]", + "finished moving funds commitment for wallet with PKH [0x%x]", sourceWalletPublicKeyHash, ) From c35f245b9d03b5cbfe4073b7deea48745ddb5aa9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 4 Dec 2023 14:40:02 +0100 Subject: [PATCH 08/50] Added description to balance check --- pkg/tbtc/node.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index c67909e128..01fd69109e 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -756,6 +756,8 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { } 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", From c410f3c66ff59f9271b300ffde66c70bcf795759 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 5 Dec 2023 13:27:39 +0100 Subject: [PATCH 09/50] Aligned unit tests --- pkg/tbtcpg/chain_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index b1e09bc9f6..3bb59ad42d 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -671,6 +671,23 @@ func (lc *LocalChain) GetWallet(walletPublicKeyHash [20]byte) ( panic("unsupported") } +func (lc *LocalChain) GetWalletParameters() ( + creationPeriod uint32, + creationMinBtcBalance uint64, + creationMaxBtcBalance uint64, + closureMinBtcBalance uint64, + maxAge uint32, + maxBtcTransfer uint64, + closingPeriod uint32, + err error, +) { + panic("unsupported") +} + +func (lc *LocalChain) GetLiveWalletsCount() (uint32, error) { + panic("unsupported") +} + func (lc *LocalChain) ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte { panic("unsupported") } From 51f268dfa910e5f473966c84223ee9fe9048b54d Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 5 Dec 2023 13:44:22 +0100 Subject: [PATCH 10/50] Improved calculation of target wallet count --- pkg/tbtc/node.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index c8738b06bd..2fc76328c1 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -791,7 +791,15 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { 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 { From a918eeb78da83f35538b6cdf50261ca99244c9bc Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 5 Dec 2023 17:05:29 +0100 Subject: [PATCH 11/50] Refactored target wallets gathering --- pkg/tbtc/node.go | 102 ++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 2fc76328c1..083c3f0317 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -717,7 +717,7 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { sourceWalletPublicKeyHash, ) - // Retrieve all the live wallets. + // Prepare the list of target wallets. liveWalletsCount, err := n.chain.GetLiveWalletsCount() if err != nil { logger.Errorf("failed to get live wallets count: [%v]", err) @@ -733,6 +733,38 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { 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( @@ -742,11 +774,13 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { return } - liveWallets := make([][20]byte, 0) + targetWallets := make([][20]byte, 0) - for _, event := range events { - walletPubKeyHash := event.WalletPublicKeyHash + 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) @@ -759,63 +793,33 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { continue } if wallet.State == StateLive { - liveWallets = append(liveWallets, walletPubKeyHash) + targetWallets = append(targetWallets, walletPubKeyHash) + } + if len(targetWallets) == int(targetWalletsCount) { + // Stop the iteration if enough live wallets have been gathered. + break } } - // Make sure all the live wallets data has been retrieved by - // comparing the number of live wallets to the on-chain counter. - if len(liveWallets) != int(liveWalletsCount) { + if len(targetWallets) != int(targetWalletsCount) { logger.Errorf( - "mismatch between the number of retrieved live wallets "+ - "and the on-chain live wallet count [%v:%v]", - len(liveWallets), - int(liveWalletsCount), + "failed to get enough target wallets: required [%v]; "+ + "gathered [%v]", + targetWalletsCount, + len(targetWallets), ) return } - // Sort the live wallets according to their numerical representation + // Sort the target wallets according to their numerical representation // as the on-chain contract expects. - sort.Slice(liveWallets, func(i, j int) bool { - bigIntI := new(big.Int).SetBytes(liveWallets[i][:]) - bigIntJ := new(big.Int).SetBytes(liveWallets[j][:]) + 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("found [%v] live wallets", len(liveWallets)) - - _, _, _, _, _, 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), - ) - - targetWallets := liveWallets[0:targetWalletsCount] - logger.Infof("target wallets length [%v]", len(targetWallets)) + logger.Infof("gathered [%v] target wallets", len(targetWallets)) walletMemberIDs := make([]uint32, 0) for _, operatorAddress := range sourceWallet.signingGroupOperators { From b26befd7b3b118bb67878e8a17b33ef8e54015c3 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 6 Dec 2023 12:23:29 +0100 Subject: [PATCH 12/50] Added caching of operator IDs --- pkg/tbtc/node.go | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 083c3f0317..58726dd254 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -10,7 +10,7 @@ import ( "sync" "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/chain" + chainpkg "github.com/keep-network/keep-core/pkg/chain" "go.uber.org/zap" @@ -76,6 +76,11 @@ type node struct { // wallet. signingExecutors map[string]*signingExecutor + operatorIDsMutex sync.Mutex + // operatorIDsCache is the cache mapping operator addresses to operator IDs. + // The cache holds IDs of singing group operators of this node. + operatorIDsCache map[chainpkg.Address]chainpkg.OperatorID + coordinationExecutorsMutex sync.Mutex // coordinationExecutors is the cache holding coordination executors for // specific wallets. The cache key is the uncompressed public key @@ -115,6 +120,7 @@ func newNode( walletDispatcher: newWalletDispatcher(), protocolLatch: latch, signingExecutors: make(map[string]*signingExecutor), + operatorIDsCache: make(map[chainpkg.Address]chainpkg.OperatorID), coordinationExecutors: make(map[string]*coordinationExecutor), proposalGenerator: proposalGenerator, } @@ -147,7 +153,7 @@ func newNode( } // operatorAddress returns the node's operator address. -func (n *node) operatorAddress() (chain.Address, error) { +func (n *node) operatorAddress() (chainpkg.Address, error) { _, operatorPublicKey, err := n.chain.OperatorKeyPair() if err != nil { return "", fmt.Errorf("failed to get operator public key: [%v]", err) @@ -165,7 +171,7 @@ func (n *node) operatorAddress() (chain.Address, error) { } // operatorAddress returns the node's operator ID. -func (n *node) operatorID() (chain.OperatorID, error) { +func (n *node) operatorID() (chainpkg.OperatorID, error) { operatorAddress, err := n.operatorAddress() if err != nil { return 0, fmt.Errorf("failed to get operator address: [%v]", err) @@ -306,6 +312,32 @@ func (n *node) getSigningExecutor( return executor, true, nil } +// getSigningGroupOperatorID gets the operator ID of the signing group operator +// based on the provided operator address. The operator ID is cached for future +// efficient retrievals. +func (n *node) getSigningGroupOperatorID( + operatorAddress chainpkg.Address, +) (chainpkg.OperatorID, error) { + n.operatorIDsMutex.Lock() + defer n.operatorIDsMutex.Unlock() + + if operatorID, exists := n.operatorIDsCache[operatorAddress]; exists { + return operatorID, nil + } + + operatorID, err := n.chain.GetOperatorID(operatorAddress) + if err != nil { + return 0, fmt.Errorf( + "failed to get operator ID for operator with address [%s]: [%v]", + operatorAddress, + err, + ) + } + + n.operatorIDsCache[operatorAddress] = operatorID + return operatorID, nil +} + // getCoordinationExecutor gets the coordination executor responsible for // executing coordination related to a specific wallet whose part is controlled // by this node. The second boolean return value indicates whether the node @@ -823,18 +855,17 @@ func (n *node) HandleMovingFundsProposal(sourceWalletPublicKeyHash [20]byte) { walletMemberIDs := make([]uint32, 0) for _, operatorAddress := range sourceWallet.signingGroupOperators { - operatorId, err := n.chain.GetOperatorID(operatorAddress) + operatorID, err := n.getSigningGroupOperatorID(operatorAddress) if err != nil { logger.Errorf( - "failed to get operator ID for operator [%v] belonging to "+ - "wallet with PKH [0x%x]: [%v]", - operatorAddress, + "failed to get operator ID belonging to wallet with "+ + "PKH [0x%x]: [%v]", sourceWalletPublicKeyHash, err, ) return } - walletMemberIDs = append(walletMemberIDs, operatorId) + walletMemberIDs = append(walletMemberIDs, operatorID) } latestBlockHeight, err := n.btcChain.GetLatestBlockHeight() From eb9a32988fc4a7f0b6674b17c5e664a5b63dd8d8 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 7 Dec 2023 18:24:54 +0100 Subject: [PATCH 13/50] Integrated moving funds proposal into coordination mechanism --- pkg/chain/ethereum/tbtc.go | 7 +++ pkg/tbtc/chain.go | 5 ++ pkg/tbtc/chain_test.go | 7 +++ pkg/tbtc/marshaling.go | 17 +++++- pkg/tbtc/moving_funds.go | 106 +++++++++++++++++++++++++++++++++++++ pkg/tbtc/node.go | 86 ++++++++++++++++++++++++++---- pkg/tbtcpg/moving_funds.go | 81 ++++++++++++++++++++++++++++ pkg/tbtcpg/tbtcpg.go | 2 +- 8 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 pkg/tbtc/moving_funds.go create mode 100644 pkg/tbtcpg/moving_funds.go diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 7c5765f7e5..17330e5fb4 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1972,3 +1972,10 @@ func (tc *TbtcChain) ValidateHeartbeatProposal( // TODO: Implementation. panic("not implemented yet") } + +func (tc *TbtcChain) ValidateMovingFundsProposal( + proposal *tbtc.MovingFundsProposal, +) error { + // TODO: Implement + return nil +} diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 486a857379..e15b87de61 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -392,6 +392,11 @@ type WalletCoordinatorChain interface { // against the chain. Returns an error if the proposal is not valid or // nil otherwise. ValidateRedemptionProposal(proposal *RedemptionProposal) error + + // 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 } // HeartbeatRequestSubmittedEvent represents a wallet heartbeat request diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index db8aca5e7e..a5732e1a6b 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -829,6 +829,13 @@ func (lc *localChain) ValidateRedemptionProposal( return nil } +func (lc *localChain) ValidateMovingFundsProposal( + proposal *MovingFundsProposal, +) error { + // TODO: Implement + return nil +} + func (lc *localChain) setRedemptionProposalValidationResult( proposal *RedemptionProposal, result bool, diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index eb6899dc45..587cb4269b 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -4,10 +4,11 @@ import ( "crypto/ecdsa" "crypto/elliptic" "fmt" - "github.com/keep-network/keep-core/pkg/bitcoin" "math" "math/big" + "github.com/keep-network/keep-core/pkg/bitcoin" + "google.golang.org/protobuf/proto" "github.com/keep-network/keep-core/pkg/chain" @@ -229,8 +230,8 @@ func unmarshalCoordinationProposal(actionType uint32, payload []byte) ( ActionHeartbeat: &HeartbeatProposal{}, ActionDepositSweep: &DepositSweepProposal{}, ActionRedemption: &RedemptionProposal{}, + ActionMovingFunds: &MovingFundsProposal{}, // TODO: Uncomment when moving funds support is implemented. - // ActionMovingFunds: &MovingFundsProposal{}, // ActionMovedFundsSweep: &MovedFundsSweepProposal{}, }[parsedActionType] if !ok { @@ -395,6 +396,18 @@ func (rp *RedemptionProposal) Unmarshal(bytes []byte) error { return nil } +// Marshal converts the movingFundsProposal to a byte array. +func (mfp *MovingFundsProposal) Marshal() ([]byte, error) { + // TODO: Implement + return nil, nil +} + +// Unmarshal converts a byte array back to the movingFundsProposal. +func (mfp *MovingFundsProposal) Unmarshal(bytes []byte) error { + // TODO: Implement + return nil +} + // marshalPublicKey converts an ECDSA public key to a byte // array (uncompressed). func marshalPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) { diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go new file mode 100644 index 0000000000..62d4a032f1 --- /dev/null +++ b/pkg/tbtc/moving_funds.go @@ -0,0 +1,106 @@ +package tbtc + +import ( + "time" + + "github.com/ipfs/go-log/v2" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "go.uber.org/zap" +) + +const ( + // TODO: Determine what the value should be. + movingFundsProposalValidityBlocks = 600 +) + +// MovingFundsProposal represents a moving funds proposal issued by a wallet's +// coordination leader. +type MovingFundsProposal struct { + // TODO: Remove WalletPublicKeyHash field. + WalletPublicKeyHash [20]byte + TargetWallets [][20]byte +} + +func (mfp *MovingFundsProposal) ActionType() WalletActionType { + return ActionMovingFunds +} + +func (mfp *MovingFundsProposal) ValidityBlocks() uint64 { + return movingFundsProposalValidityBlocks +} + +type MovingFundsRequest struct { + TargetWalletPublicKeyHash [20]byte +} + +// movingFundsAction is a walletAction implementation handling moving funds +// requests from the wallet coordinator. +type movingFundsAction struct { + logger *zap.SugaredLogger + chain Chain + btcChain bitcoin.Chain + + movingFundsWallet wallet + transactionExecutor *walletTransactionExecutor + + proposal *MovingFundsProposal + proposalProcessingStartBlock uint64 + proposalExpiryBlock uint64 + + broadcastTimeout time.Duration + broadcastCheckDelay time.Duration +} + +func newMovingFundsAction( + logger *zap.SugaredLogger, + chain Chain, + btcChain bitcoin.Chain, + movingFundsWallet wallet, + signingExecutor walletSigningExecutor, + proposal *MovingFundsProposal, + proposalProcessingStartBlock uint64, + proposalExpiryBlock uint64, + waitForBlockFn waitForBlockFn, +) *movingFundsAction { + transactionExecutor := newWalletTransactionExecutor( + btcChain, + movingFundsWallet, + signingExecutor, + waitForBlockFn, + ) + + return &movingFundsAction{ + logger: logger, + chain: chain, + btcChain: btcChain, + movingFundsWallet: movingFundsWallet, + transactionExecutor: transactionExecutor, + proposal: proposal, + proposalProcessingStartBlock: proposalProcessingStartBlock, + proposalExpiryBlock: proposalExpiryBlock, + } +} + +func (mfa *movingFundsAction) execute() error { + // TODO: Implement + return nil +} + +// ValidateMovingFundsProposal checks the moving funds proposal with on-chain +// validation rules. +func ValidateMovingFundsProposal( + validateProposalLogger log.StandardLogger, + proposal *MovingFundsProposal, +) ([]*MovingFundsRequest, error) { + // TODO: Implement + return nil, nil +} + +func (mfa *movingFundsAction) wallet() wallet { + return mfa.movingFundsWallet +} + +func (mfa *movingFundsAction) actionType() WalletActionType { + return ActionMovingFunds +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 58726dd254..5532a55a7e 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -645,6 +645,74 @@ func (n *node) handleRedemptionProposal( walletActionLogger.Infof("wallet action dispatched successfully") } +func (n *node) handleMovingFundsProposal( + wallet wallet, + proposal *MovingFundsProposal, + startBlock uint64, + expiryBlock uint64, +) { + signingExecutor, ok, err := n.getSigningExecutor(wallet.publicKey) + if err != nil { + logger.Errorf("cannot get signing executor: [%v]", err) + return + } + // This check is actually redundant. We know the node controls some + // wallet signers as we just got the wallet from the registry using their + // public key hash. However, we are doing it just in case. The API + // contract of getSigningExecutor may change one day. + if !ok { + logger.Infof( + "node does not control signers of wallet PKH [0x%x]; "+ + "ignoring the received moving funds proposal", + proposal.WalletPublicKeyHash, + ) + return + } + + walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) + if err != nil { + logger.Errorf("cannot marshal wallet public key: [%v]", err) + return + } + + logger.Infof( + "node controls signers of wallet PKH [0x%x]; "+ + "plain-text uncompressed public key of that wallet is [0x%x]; "+ + "starting orchestration of the moving funds action", + proposal.WalletPublicKeyHash, + walletPublicKeyBytes, + ) + + walletActionLogger := logger.With( + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + zap.String("action", ActionMovingFunds.String()), + zap.Uint64("startBlock", startBlock), + zap.Uint64("expiryBlock", expiryBlock), + ) + walletActionLogger.Infof("dispatching wallet action") + + action := newMovingFundsAction( + walletActionLogger, + n.chain, + n.btcChain, + wallet, + signingExecutor, + proposal, + startBlock, + expiryBlock, + n.waitForBlockHeight, + ) + + err = n.walletDispatcher.dispatch(action) + if err != nil { + walletActionLogger.Errorf("cannot dispatch wallet action: [%v]", err) + return + } + + 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( @@ -1088,16 +1156,16 @@ func processCoordinationResult(node *node, result *coordinationResult) { expiryBlock, ) } + case ActionMovingFunds: + if proposal, ok := result.proposal.(*MovingFundsProposal); ok { + node.handleMovingFundsProposal( + result.wallet, + proposal, + startBlock, + expiryBlock, + ) + } // TODO: Uncomment when moving funds support is implemented. - // case ActionMovingFunds: - // if proposal, ok := result.proposal.(*MovingFundsProposal); ok { - // node.handleMovingFundsProposal( - // result.wallet, - // proposal, - // startBlock, - // expiryBlock, - // ) - // } // case ActionMovedFundsSweep: // if proposal, ok := result.proposal.(*MovedFundsSweepProposal); ok { // node.handleMovedFundsSweepProposal( diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go new file mode 100644 index 0000000000..f407ec1f3e --- /dev/null +++ b/pkg/tbtcpg/moving_funds.go @@ -0,0 +1,81 @@ +package tbtcpg + +import ( + "fmt" + + "go.uber.org/zap" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +// MovingFundsTask is a task that may produce a moving funds proposal. +type MovingFundsTask struct { + chain Chain + btcChain bitcoin.Chain +} + +func NewMovingFundsTask( + chain Chain, + btcChain bitcoin.Chain, +) *MovingFundsTask { + return &MovingFundsTask{ + chain: chain, + btcChain: btcChain, + } +} + +func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( + tbtc.CoordinationProposal, + bool, + error, +) { + walletPublicKeyHash := request.WalletPublicKeyHash + + taskLogger := logger.With( + zap.String("task", mft.ActionType().String()), + zap.String("walletPKH", fmt.Sprintf("0x%x", walletPublicKeyHash)), + ) + + proposal, err := mft.ProposeMovingFunds( + taskLogger, + walletPublicKeyHash, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot prepare moving funds proposal: [%w]", + err, + ) + } + + return proposal, false, nil +} + +func (mft *MovingFundsTask) ProposeMovingFunds( + taskLogger log.StandardLogger, + walletPublicKeyHash [20]byte, +) (*tbtc.MovingFundsProposal, error) { + taskLogger.Infof("preparing a moving funds proposal") + + proposal := &tbtc.MovingFundsProposal{ + WalletPublicKeyHash: walletPublicKeyHash, + } + + taskLogger.Infof("validating the moving funds proposal") + if _, err := tbtc.ValidateMovingFundsProposal( + taskLogger, + proposal, + ); err != nil { + return nil, fmt.Errorf( + "failed to verify moving funds proposal: %v", + err, + ) + } + + return proposal, nil +} + +func (mft *MovingFundsTask) ActionType() tbtc.WalletActionType { + return tbtc.ActionMovingFunds +} diff --git a/pkg/tbtcpg/tbtcpg.go b/pkg/tbtcpg/tbtcpg.go index 24d5434b3e..f14cd1f7f4 100644 --- a/pkg/tbtcpg/tbtcpg.go +++ b/pkg/tbtcpg/tbtcpg.go @@ -40,9 +40,9 @@ func NewProposalGenerator( NewDepositSweepTask(chain, btcChain), NewRedemptionTask(chain, btcChain), NewHeartbeatTask(chain), + NewMovingFundsTask(chain, btcChain), // TODO: Uncomment when moving funds support is implemented. // newMovedFundsSweepTask(), - // newMovingFundsTask(), } return &ProposalGenerator{ From 590e84d50093b58c02c99bb8901c0880dd20635a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 11 Dec 2023 14:08:54 +0100 Subject: [PATCH 14/50] Implemented move funds task --- pkg/tbtc/moving_funds.go | 5 +- pkg/tbtc/node.go | 267 ------------------------------------- pkg/tbtcpg/moving_funds.go | 122 +++++++++++++++++ 3 files changed, 126 insertions(+), 268 deletions(-) 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") From d9437303b9715a374318405dac6075d79275689e Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 4 Jan 2024 19:03:35 +0100 Subject: [PATCH 15/50] Added moving funds message unmarshaling --- pkg/tbtc/gen/pb/message.pb.go | 105 ++++++++++++++++++++++++++++++---- pkg/tbtc/gen/pb/message.proto | 8 ++- pkg/tbtc/marshaling.go | 33 +++++++++-- pkg/tbtc/marshaling_test.go | 73 ++++++++++++++++++----- pkg/tbtc/moving_funds.go | 3 + 5 files changed, 194 insertions(+), 28 deletions(-) diff --git a/pkg/tbtc/gen/pb/message.pb.go b/pkg/tbtc/gen/pb/message.pb.go index 424df0eacd..82068dd9a3 100644 --- a/pkg/tbtc/gen/pb/message.pb.go +++ b/pkg/tbtc/gen/pb/message.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 -// protoc v3.7.1 +// protoc-gen-go v1.28.0 +// protoc v3.19.4 // source: pkg/tbtc/gen/pb/message.proto package pb @@ -390,6 +390,69 @@ func (x *RedemptionProposal) GetRedemptionTxFee() []byte { return nil } +type MovingFundsProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WalletPublicKeyHash []byte `protobuf:"bytes,1,opt,name=walletPublicKeyHash,proto3" json:"walletPublicKeyHash,omitempty"` + TargetWallets [][]byte `protobuf:"bytes,2,rep,name=targetWallets,proto3" json:"targetWallets,omitempty"` + MovingFundsTxFee []byte `protobuf:"bytes,3,opt,name=movingFundsTxFee,proto3" json:"movingFundsTxFee,omitempty"` +} + +func (x *MovingFundsProposal) Reset() { + *x = MovingFundsProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MovingFundsProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MovingFundsProposal) ProtoMessage() {} + +func (x *MovingFundsProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MovingFundsProposal.ProtoReflect.Descriptor instead. +func (*MovingFundsProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{6} +} + +func (x *MovingFundsProposal) GetWalletPublicKeyHash() []byte { + if x != nil { + return x.WalletPublicKeyHash + } + return nil +} + +func (x *MovingFundsProposal) GetTargetWallets() [][]byte { + if x != nil { + return x.TargetWallets + } + return nil +} + +func (x *MovingFundsProposal) GetMovingFundsTxFee() []byte { + if x != nil { + return x.MovingFundsTxFee + } + return nil +} + type DepositSweepProposal_DepositKey struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -402,7 +465,7 @@ type DepositSweepProposal_DepositKey struct { func (x *DepositSweepProposal_DepositKey) Reset() { *x = DepositSweepProposal_DepositKey{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[6] + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -415,7 +478,7 @@ func (x *DepositSweepProposal_DepositKey) String() string { func (*DepositSweepProposal_DepositKey) ProtoMessage() {} func (x *DepositSweepProposal_DepositKey) ProtoReflect() protoreflect.Message { - mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[6] + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -507,8 +570,17 @@ var file_pkg_tbtc_gen_pb_message_proto_rawDesc = []byte{ 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x72, 0x65, 0x64, 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x64, 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, - 0x46, 0x65, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x46, 0x65, 0x65, 0x22, 0x99, 0x01, 0x0a, 0x13, 0x4d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, + 0x6e, 0x64, 0x73, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x30, 0x0a, 0x13, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, + 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x24, 0x0a, + 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, + 0x64, 0x73, 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x6d, + 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, 0x64, 0x73, 0x54, 0x78, 0x46, 0x65, 0x65, 0x42, + 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -523,7 +595,7 @@ func file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP() []byte { return file_pkg_tbtc_gen_pb_message_proto_rawDescData } -var file_pkg_tbtc_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_pkg_tbtc_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_pkg_tbtc_gen_pb_message_proto_goTypes = []interface{}{ (*SigningDoneMessage)(nil), // 0: tbtc.SigningDoneMessage (*CoordinationProposal)(nil), // 1: tbtc.CoordinationProposal @@ -531,11 +603,12 @@ var file_pkg_tbtc_gen_pb_message_proto_goTypes = []interface{}{ (*HeartbeatProposal)(nil), // 3: tbtc.HeartbeatProposal (*DepositSweepProposal)(nil), // 4: tbtc.DepositSweepProposal (*RedemptionProposal)(nil), // 5: tbtc.RedemptionProposal - (*DepositSweepProposal_DepositKey)(nil), // 6: tbtc.DepositSweepProposal.DepositKey + (*MovingFundsProposal)(nil), // 6: tbtc.MovingFundsProposal + (*DepositSweepProposal_DepositKey)(nil), // 7: tbtc.DepositSweepProposal.DepositKey } var file_pkg_tbtc_gen_pb_message_proto_depIdxs = []int32{ 1, // 0: tbtc.CoordinationMessage.proposal:type_name -> tbtc.CoordinationProposal - 6, // 1: tbtc.DepositSweepProposal.depositsKeys:type_name -> tbtc.DepositSweepProposal.DepositKey + 7, // 1: tbtc.DepositSweepProposal.depositsKeys:type_name -> tbtc.DepositSweepProposal.DepositKey 2, // [2:2] is the sub-list for method output_type 2, // [2:2] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name @@ -622,6 +695,18 @@ func file_pkg_tbtc_gen_pb_message_proto_init() { } } file_pkg_tbtc_gen_pb_message_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MovingFundsProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DepositSweepProposal_DepositKey); i { case 0: return &v.state @@ -640,7 +725,7 @@ func file_pkg_tbtc_gen_pb_message_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_pkg_tbtc_gen_pb_message_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/tbtc/gen/pb/message.proto b/pkg/tbtc/gen/pb/message.proto index 1da799a18f..beba80ea45 100644 --- a/pkg/tbtc/gen/pb/message.proto +++ b/pkg/tbtc/gen/pb/message.proto @@ -41,4 +41,10 @@ message DepositSweepProposal { message RedemptionProposal { repeated bytes redeemersOutputScripts = 1; bytes redemptionTxFee = 2; -} \ No newline at end of file +} + +message MovingFundsProposal { + bytes walletPublicKeyHash = 1; + repeated bytes targetWallets = 2; + bytes movingFundsTxFee = 3; +} diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 587cb4269b..6c970a1e37 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -398,13 +398,38 @@ func (rp *RedemptionProposal) Unmarshal(bytes []byte) error { // Marshal converts the movingFundsProposal to a byte array. func (mfp *MovingFundsProposal) Marshal() ([]byte, error) { - // TODO: Implement - return nil, nil + targetWallets := make([][]byte, len(mfp.TargetWallets)) + + for i, wallet := range mfp.TargetWallets { + targetWallet := make([]byte, len(wallet)) + copy(targetWallet, wallet[:]) + + targetWallets[i] = targetWallet + } + + return proto.Marshal( + &pb.MovingFundsProposal{ + WalletPublicKeyHash: mfp.WalletPublicKeyHash[:], + TargetWallets: targetWallets, + MovingFundsTxFee: mfp.MovingFundsTxFee.Bytes(), + }) } // Unmarshal converts a byte array back to the movingFundsProposal. -func (mfp *MovingFundsProposal) Unmarshal(bytes []byte) error { - // TODO: Implement +func (mfp *MovingFundsProposal) Unmarshal(data []byte) error { + pbMsg := pb.MovingFundsProposal{} + if err := proto.Unmarshal(data, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal MovingFundsProposal: [%v]", err) + } + + copy(mfp.WalletPublicKeyHash[:], pbMsg.WalletPublicKeyHash) + + mfp.TargetWallets = make([][20]byte, len(pbMsg.TargetWallets)) + for i, wallet := range pbMsg.TargetWallets { + copy(mfp.TargetWallets[i][:], wallet) + } + + mfp.MovingFundsTxFee = new(big.Int).SetBytes(pbMsg.MovingFundsTxFee) return nil } diff --git a/pkg/tbtc/marshaling_test.go b/pkg/tbtc/marshaling_test.go index 9092eb2ef4..c686f30a1c 100644 --- a/pkg/tbtc/marshaling_test.go +++ b/pkg/tbtc/marshaling_test.go @@ -4,11 +4,12 @@ import ( "crypto/ecdsa" "crypto/elliptic" "encoding/hex" - "github.com/keep-network/keep-core/pkg/bitcoin" "math/big" "reflect" "testing" + "github.com/keep-network/keep-core/pkg/bitcoin" + fuzz "github.com/google/gofuzz" "github.com/keep-network/keep-core/internal/testutils" @@ -126,6 +127,21 @@ func TestCoordinationMessage_MarshalingRoundtrip(t *testing.T) { return parsed } + toByte20 := func(s string) [20]byte { + bytes, err := hex.DecodeString(s) + if err != nil { + t.Fatal(err) + } + + if len(bytes) != 20 { + t.Fatal("incorrect hexstring length") + } + + var result [20]byte + copy(result[:], bytes[:]) + return result + } + tests := map[string]struct { proposal CoordinationProposal }{ @@ -168,23 +184,25 @@ func TestCoordinationMessage_MarshalingRoundtrip(t *testing.T) { RedemptionTxFee: big.NewInt(10000), }, }, - // TODO: Uncomment when moving funds support is implemented. - // "with moving funds proposal": { - // proposal: &MovingFundsProposal{}, - // }, + "with moving funds proposal": { + proposal: &MovingFundsProposal{ + WalletPublicKeyHash: toByte20( + "da7c1fb602db1931a3b815563b6f6fae6a58f224", + ), + TargetWallets: [][20]byte{ + toByte20("cb7d88a87c37aff0c1535fa4efe6f0a2406ea5e9"), + toByte20("f87eb7ec3b15a3fdd7b57754d765694b3e0b4bf4"), + }, + MovingFundsTxFee: big.NewInt(10000), + }, + }, + // TODO: Uncomment when moved funds sweep support is implemented. // "with moved funds sweep proposal": { // proposal: &MovedFundsSweepProposal{}, // }, } - walletPublicKeyHashBytes, err := hex.DecodeString( - "aa768412ceed10bd423c025542ca90071f9fb62d", - ) - if err != nil { - t.Fatal(err) - } - var walletPublicKeyHash [20]byte - copy(walletPublicKeyHash[:], walletPublicKeyHashBytes) + walletPublicKeyHash := toByte20("aa768412ceed10bd423c025542ca90071f9fb62d") for testName, test := range tests { t.Run(testName, func(t *testing.T) { @@ -295,6 +313,35 @@ func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithRedemptionProposal(t *t } } +func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithMovingFundsProposal(t *testing.T) { + for i := 0; i < 10; i++ { + var ( + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal MovingFundsProposal + ) + + f := fuzz.New().NilChance(0.1). + NumElements(0, 512). + Funcs(pbutils.FuzzFuncs()...) + + f.Fuzz(&senderID) + f.Fuzz(&coordinationBlock) + f.Fuzz(&walletPublicKeyHash) + f.Fuzz(&proposal) + + doneMessage := &coordinationMessage{ + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: walletPublicKeyHash, + proposal: &proposal, + } + + _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + } +} + func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithNoopProposal(t *testing.T) { for i := 0; i < 10; i++ { var ( diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 6a776211a6..b2658de17c 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -1,6 +1,8 @@ package tbtc import ( + "math/big" + "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" @@ -18,6 +20,7 @@ type MovingFundsProposal struct { // TODO: Remove WalletPublicKeyHash field. WalletPublicKeyHash [20]byte TargetWallets [][20]byte + MovingFundsTxFee *big.Int } func (mfp *MovingFundsProposal) ActionType() WalletActionType { From f8bdbef843dd01908df76fa9292c8bec6496700a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 4 Jan 2024 19:31:55 +0100 Subject: [PATCH 16/50] Removed wallet public key hash from moving funds proposal --- pkg/tbtc/gen/pb/message.pb.go | 31 ++++++++++--------------------- pkg/tbtc/gen/pb/message.proto | 5 ++--- pkg/tbtc/marshaling.go | 7 ++----- pkg/tbtc/marshaling_test.go | 3 --- pkg/tbtc/moving_funds.go | 6 ++---- pkg/tbtc/node.go | 21 ++++++++++----------- pkg/tbtcpg/moving_funds.go | 6 ++---- 7 files changed, 28 insertions(+), 51 deletions(-) diff --git a/pkg/tbtc/gen/pb/message.pb.go b/pkg/tbtc/gen/pb/message.pb.go index 82068dd9a3..03a02f70ee 100644 --- a/pkg/tbtc/gen/pb/message.pb.go +++ b/pkg/tbtc/gen/pb/message.pb.go @@ -395,9 +395,8 @@ type MovingFundsProposal struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - WalletPublicKeyHash []byte `protobuf:"bytes,1,opt,name=walletPublicKeyHash,proto3" json:"walletPublicKeyHash,omitempty"` - TargetWallets [][]byte `protobuf:"bytes,2,rep,name=targetWallets,proto3" json:"targetWallets,omitempty"` - MovingFundsTxFee []byte `protobuf:"bytes,3,opt,name=movingFundsTxFee,proto3" json:"movingFundsTxFee,omitempty"` + TargetWallets [][]byte `protobuf:"bytes,1,rep,name=targetWallets,proto3" json:"targetWallets,omitempty"` + MovingFundsTxFee []byte `protobuf:"bytes,2,opt,name=movingFundsTxFee,proto3" json:"movingFundsTxFee,omitempty"` } func (x *MovingFundsProposal) Reset() { @@ -432,13 +431,6 @@ func (*MovingFundsProposal) Descriptor() ([]byte, []int) { return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{6} } -func (x *MovingFundsProposal) GetWalletPublicKeyHash() []byte { - if x != nil { - return x.WalletPublicKeyHash - } - return nil -} - func (x *MovingFundsProposal) GetTargetWallets() [][]byte { if x != nil { return x.TargetWallets @@ -570,17 +562,14 @@ var file_pkg_tbtc_gen_pb_message_proto_rawDesc = []byte{ 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x72, 0x65, 0x64, 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x64, 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, - 0x46, 0x65, 0x65, 0x22, 0x99, 0x01, 0x0a, 0x13, 0x4d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, - 0x6e, 0x64, 0x73, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x30, 0x0a, 0x13, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, - 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x24, 0x0a, - 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, - 0x64, 0x73, 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x6d, - 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, 0x64, 0x73, 0x54, 0x78, 0x46, 0x65, 0x65, 0x42, - 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x46, 0x65, 0x65, 0x22, 0x67, 0x0a, 0x13, 0x4d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, + 0x64, 0x73, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0c, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x73, + 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, 0x64, 0x73, 0x54, + 0x78, 0x46, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x6d, 0x6f, 0x76, 0x69, + 0x6e, 0x67, 0x46, 0x75, 0x6e, 0x64, 0x73, 0x54, 0x78, 0x46, 0x65, 0x65, 0x42, 0x06, 0x5a, 0x04, + 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/tbtc/gen/pb/message.proto b/pkg/tbtc/gen/pb/message.proto index beba80ea45..d8aa30357e 100644 --- a/pkg/tbtc/gen/pb/message.proto +++ b/pkg/tbtc/gen/pb/message.proto @@ -44,7 +44,6 @@ message RedemptionProposal { } message MovingFundsProposal { - bytes walletPublicKeyHash = 1; - repeated bytes targetWallets = 2; - bytes movingFundsTxFee = 3; + repeated bytes targetWallets = 1; + bytes movingFundsTxFee = 2; } diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 6c970a1e37..9eea456447 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -409,9 +409,8 @@ func (mfp *MovingFundsProposal) Marshal() ([]byte, error) { return proto.Marshal( &pb.MovingFundsProposal{ - WalletPublicKeyHash: mfp.WalletPublicKeyHash[:], - TargetWallets: targetWallets, - MovingFundsTxFee: mfp.MovingFundsTxFee.Bytes(), + TargetWallets: targetWallets, + MovingFundsTxFee: mfp.MovingFundsTxFee.Bytes(), }) } @@ -422,8 +421,6 @@ func (mfp *MovingFundsProposal) Unmarshal(data []byte) error { return fmt.Errorf("failed to unmarshal MovingFundsProposal: [%v]", err) } - copy(mfp.WalletPublicKeyHash[:], pbMsg.WalletPublicKeyHash) - mfp.TargetWallets = make([][20]byte, len(pbMsg.TargetWallets)) for i, wallet := range pbMsg.TargetWallets { copy(mfp.TargetWallets[i][:], wallet) diff --git a/pkg/tbtc/marshaling_test.go b/pkg/tbtc/marshaling_test.go index c686f30a1c..e9fc678e9a 100644 --- a/pkg/tbtc/marshaling_test.go +++ b/pkg/tbtc/marshaling_test.go @@ -186,9 +186,6 @@ func TestCoordinationMessage_MarshalingRoundtrip(t *testing.T) { }, "with moving funds proposal": { proposal: &MovingFundsProposal{ - WalletPublicKeyHash: toByte20( - "da7c1fb602db1931a3b815563b6f6fae6a58f224", - ), TargetWallets: [][20]byte{ toByte20("cb7d88a87c37aff0c1535fa4efe6f0a2406ea5e9"), toByte20("f87eb7ec3b15a3fdd7b57754d765694b3e0b4bf4"), diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index b2658de17c..0791647bdf 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -17,10 +17,8 @@ const ( // MovingFundsProposal represents a moving funds proposal issued by a wallet's // coordination leader. type MovingFundsProposal struct { - // TODO: Remove WalletPublicKeyHash field. - WalletPublicKeyHash [20]byte - TargetWallets [][20]byte - MovingFundsTxFee *big.Int + TargetWallets [][20]byte + MovingFundsTxFee *big.Int } func (mfp *MovingFundsProposal) ActionType() WalletActionType { diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 5917dc3884..686207e26a 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -646,6 +646,12 @@ func (n *node) handleMovingFundsProposal( startBlock uint64, expiryBlock uint64, ) { + walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) + if err != nil { + logger.Errorf("cannot marshal wallet public key: [%v]", err) + return + } + signingExecutor, ok, err := n.getSigningExecutor(wallet.publicKey) if err != nil { logger.Errorf("cannot get signing executor: [%v]", err) @@ -659,23 +665,16 @@ func (n *node) handleMovingFundsProposal( logger.Infof( "node does not control signers of wallet PKH [0x%x]; "+ "ignoring the received moving funds proposal", - proposal.WalletPublicKeyHash, + walletPublicKeyBytes, ) return } - walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) - if err != nil { - logger.Errorf("cannot marshal wallet public key: [%v]", err) - return - } - logger.Infof( - "node controls signers of wallet PKH [0x%x]; "+ - "plain-text uncompressed public key of that wallet is [0x%x]; "+ - "starting orchestration of the moving funds action", - proposal.WalletPublicKeyHash, + "starting orchestration of the moving funds action for wallet [0x%x]; "+ + "20-byte public key hash of that wallet is [0x%x]", walletPublicKeyBytes, + bitcoin.PublicKeyHash(wallet.publicKey), ) walletActionLogger := logger.With( diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 37e2402b5e..880fef7064 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -99,7 +99,6 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( proposal, err := mft.ProposeMovingFunds( taskLogger, - walletPublicKeyHash, targetWallets, ) if err != nil { @@ -174,14 +173,13 @@ func (mft *MovingFundsTask) SubmitMovingFundsCommitment() ([][20]byte, error) { 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, + TargetWallets: targetWallets, + // TODO: Add fee } taskLogger.Infof("validating the moving funds proposal") From 6710e7335facb79dacf6c5f1497618a4c317b444 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 5 Jan 2024 18:12:46 +0100 Subject: [PATCH 17/50] Added finding target wallets --- pkg/chain/ethereum/tbtc.go | 45 +++++++ pkg/tbtc/chain.go | 44 +++---- pkg/tbtc/chain_test.go | 15 ++- pkg/tbtcpg/chain.go | 33 +++++ pkg/tbtcpg/chain_test.go | 22 ++++ pkg/tbtcpg/moving_funds.go | 255 ++++++++++++++++++++++++++++++------- 6 files changed, 337 insertions(+), 77 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 6d35720cbc..f3625669a5 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1484,6 +1484,50 @@ func (tc *TbtcChain) GetLiveWalletsCount() (uint32, error) { return tc.bridge.LiveWalletsCount() } +func (tc *TbtcChain) PastMovingFundsCommitmentSubmittedEvents( + filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, +) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) { + var startBlock uint64 + var endBlock *uint64 + var walletPublicKeyHash [][20]byte + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + walletPublicKeyHash = filter.WalletPublicKeyHash + } + + events, err := tc.bridge.PastMovingFundsCommitmentSubmittedEvents( + startBlock, + endBlock, + walletPublicKeyHash, + ) + if err != nil { + return nil, err + } + + convertedEvents := make([]*tbtc.MovingFundsCommitmentSubmittedEvent, 0) + for _, event := range events { + convertedEvent := &tbtc.MovingFundsCommitmentSubmittedEvent{ + WalletPublicKeyHash: event.WalletPubKeyHash, + TargetWallets: event.TargetWallets, + Submitter: chain.Address(event.Submitter.Hex()), + BlockNumber: event.Raw.BlockNumber, + } + + convertedEvents = append(convertedEvents, convertedEvent) + } + + sort.SliceStable( + convertedEvents, + func(i, j int) bool { + return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber + }, + ) + + return convertedEvents, err +} + func buildDepositKey( fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, @@ -1705,6 +1749,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..a675cf6a85 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. @@ -412,6 +391,21 @@ type RedemptionRequestedEventFilter struct { Redeemer []chain.Address } +// MovingFundsCommitmentSubmittedEvent represents a moving funds commitment submitted event. +type MovingFundsCommitmentSubmittedEvent struct { + WalletPublicKeyHash [20]byte + TargetWallets [][20]byte + Submitter chain.Address + BlockNumber uint64 +} + +// MovingFundsCommitmentSubmittedEventFilter is a component allowing to filter MovingFundsCommitmentSubmittedEvent. +type MovingFundsCommitmentSubmittedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + WalletPublicKeyHash [][20]byte +} + // Chain represents the interface that the TBTC module expects to interact // with the anchoring blockchain on. type Chain interface { 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..0e9db9b249 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,21 @@ type Chain interface { walletPublicKeyHash [20]byte, proposal *tbtc.HeartbeatProposal, ) error + + // PastMovingFundsCommitmentSubmittedEvents fetches past moving funds + // commitment submitted 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. + PastMovingFundsCommitmentSubmittedEvents( + filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, + ) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, 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..4555724203 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, @@ -711,6 +727,12 @@ func (lc *LocalChain) ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOu panic("unsupported") } +func (lc *LocalChain) PastMovingFundsCommitmentSubmittedEvents( + filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, +) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) { + panic("unsupported") +} + func (lc *LocalChain) SubmitMovingFundsCommitment( walletPublicKeyHash [20]byte, walletMainUTXO bitcoin.UnspentTransactionOutput, diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 880fef7064..35b0869fe7 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,202 @@ 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. + targetWallets, err := mft.findNewTargetWallets( + taskLogger, + sourceWalletPublicKeyHash, + walletBalance, + liveWalletsCount, + ) + return targetWallets, false, err } - return nil, false, nil + + targetWallets, err := mft.retrieveCommittedTargetWallets( + taskLogger, + sourceWalletPublicKeyHash, + ) + return targetWallets, true, err } -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, error) { + taskLogger.Infof( + "commitment not submitted yet; looking for new target wallets", + ) + + _, _, _, _, _, walletMaxBtcTransfer, _, err := mft.chain.GetWalletParameters() + if err != nil { + return nil, 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, 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, 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 + } + } + + if len(targetWallets) != int(targetWalletsCount) { + return nil, 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, nil +} + +func (mft *MovingFundsTask) retrieveCommittedTargetWallets( + taskLogger log.StandardLogger, + sourceWalletPublicKeyHash [20]byte, +) ([][20]byte, error) { + taskLogger.Infof( + "commitment already submitted; retrieving committed target wallets", + ) + + blockCounter, err := mft.chain.BlockCounter() + if err != nil { + return nil, fmt.Errorf( + "failed to get block counter: [%w]", + err, + ) + } + + currentBlockNumber, err := blockCounter.CurrentBlock() + if err != nil { + return nil, fmt.Errorf( + "failed to get current block number: [%w]", + err, + ) + } + + // Look back one month when searching for the commitment event. + filterLookBackSeconds := uint64(30 * 24 * 60 * 60) + filterLookBackBlocks := filterLookBackSeconds / uint64(mft.chain.AverageBlockTime().Seconds()) + filterStartBlock := currentBlockNumber - filterLookBackBlocks + + filter := &tbtc.MovingFundsCommitmentSubmittedEventFilter{ + StartBlock: filterStartBlock, + WalletPublicKeyHash: [][20]byte{sourceWalletPublicKeyHash}, + } + + events, err := mft.chain.PastMovingFundsCommitmentSubmittedEvents(filter) + if err != nil { + return nil, fmt.Errorf( + "failed to get past moving funds commitment submitted events: [%w]", + err, + ) + } + + // Moving funds commitment can be submitted only once for a given wallet. + // Check just in case. + if len(events) != 1 { + return nil, fmt.Errorf( + "unexpected number of moving funds commitment submitted events: [%v]", + len(events), + ) + } + + targetWallets := events[0].TargetWallets + + // Make sure all the target wallets are Live. + for _, targetWallet := range targetWallets { + targetWalletChainData, err := mft.chain.GetWallet(targetWallet) + if err != nil { + return nil, fmt.Errorf( + "failed to get wallet data for target wallet [0x%x]: [%w]", + targetWallet, + err, + ) + } + + if targetWalletChainData.State != tbtc.StateLive { + return nil, fmt.Errorf( + "target wallet [0x%x] is not Live", + targetWallet, + ) + } } - return true, nil + return targetWallets, nil } func (mft *MovingFundsTask) SubmitMovingFundsCommitment() ([][20]byte, error) { From bec8275b174c9fe6d307960e43999f8be50b1c2d Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 9 Jan 2024 12:13:19 +0100 Subject: [PATCH 18/50] Fixed linting errors --- pkg/tbtc/node.go | 32 -------------------------------- pkg/tbtcpg/moving_funds.go | 8 +++++++- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 686207e26a..a1086f8f01 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -75,11 +75,6 @@ type node struct { // wallet. signingExecutors map[string]*signingExecutor - operatorIDsMutex sync.Mutex - // operatorIDsCache is the cache mapping operator addresses to operator IDs. - // The cache holds IDs of singing group operators of this node. - operatorIDsCache map[chainpkg.Address]chainpkg.OperatorID - coordinationExecutorsMutex sync.Mutex // coordinationExecutors is the cache holding coordination executors for // specific wallets. The cache key is the uncompressed public key @@ -119,7 +114,6 @@ func newNode( walletDispatcher: newWalletDispatcher(), protocolLatch: latch, signingExecutors: make(map[string]*signingExecutor), - operatorIDsCache: make(map[chainpkg.Address]chainpkg.OperatorID), coordinationExecutors: make(map[string]*coordinationExecutor), proposalGenerator: proposalGenerator, } @@ -311,32 +305,6 @@ func (n *node) getSigningExecutor( return executor, true, nil } -// getSigningGroupOperatorID gets the operator ID of the signing group operator -// based on the provided operator address. The operator ID is cached for future -// efficient retrievals. -func (n *node) getSigningGroupOperatorID( - operatorAddress chainpkg.Address, -) (chainpkg.OperatorID, error) { - n.operatorIDsMutex.Lock() - defer n.operatorIDsMutex.Unlock() - - if operatorID, exists := n.operatorIDsCache[operatorAddress]; exists { - return operatorID, nil - } - - operatorID, err := n.chain.GetOperatorID(operatorAddress) - if err != nil { - return 0, fmt.Errorf( - "failed to get operator ID for operator with address [%s]: [%v]", - operatorAddress, - err, - ) - } - - n.operatorIDsCache[operatorAddress] = operatorID - return operatorID, nil -} - // getCoordinationExecutor gets the coordination executor responsible for // executing coordination related to a specific wallet whose part is controlled // by this node. The second boolean return value indicates whether the node diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 35b0869fe7..5134f88cdb 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -124,7 +124,13 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( } if !commitmentSubmitted { - mft.SubmitMovingFundsCommitment() + _, err := mft.SubmitMovingFundsCommitment() + if err != nil { + return nil, false, fmt.Errorf( + "error while submitting moving funds commitment: [%w]", + err, + ) + } } return proposal, false, nil From f3250b30ea836b3859332d4f3b86c00e033f267f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 9 Jan 2024 14:52:01 +0100 Subject: [PATCH 19/50] Added submitting moved funds commitment --- pkg/tbtc/coordination.go | 2 + pkg/tbtcpg/chain.go | 3 ++ pkg/tbtcpg/chain_test.go | 6 +++ pkg/tbtcpg/moving_funds.go | 77 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 9f0fa4ea0f..2dc909b14a 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -200,6 +200,7 @@ func (cf *coordinationFault) String() string { type CoordinationProposalRequest struct { WalletPublicKeyHash [20]byte WalletOperators []chain.Address + ExecutingOperator chain.Address ActionsChecklist []WalletActionType } @@ -581,6 +582,7 @@ func (ce *coordinationExecutor) executeLeaderRoutine( &CoordinationProposalRequest{ WalletPublicKeyHash: walletPublicKeyHash, WalletOperators: ce.coordinatedWallet.signingGroupOperators, + ExecutingOperator: ce.operatorAddress, ActionsChecklist: actionsChecklist, }, ) diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 0e9db9b249..14742d1fdc 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -120,6 +120,9 @@ type Chain interface { AverageBlockTime() time.Duration + // GetOperatorID returns the operator ID for the given operator address. + GetOperatorID(operatorAddress chain.Address) (chain.OperatorID, error) + // ValidateHeartbeatProposal validates the given heartbeat proposal // against the chain. Returns an error if the proposal is not valid or // nil otherwise. diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 4555724203..1d86e01926 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -692,6 +692,12 @@ func (lc *LocalChain) AverageBlockTime() time.Duration { return lc.averageBlockTime } +func (lc *LocalChain) GetOperatorID( + operatorAddress chain.Address, +) (chain.OperatorID, error) { + panic("unsupported") +} + func (lc *LocalChain) SetAverageBlockTime(averageBlockTime time.Duration) { lc.mutex.Lock() defer lc.mutex.Unlock() diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 5134f88cdb..a0d3af7d31 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -9,6 +9,7 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" ) @@ -124,7 +125,24 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( } if !commitmentSubmitted { - _, err := mft.SubmitMovingFundsCommitment() + walletMemberIDs, walletMemberIndex, err := mft.GetWalletMembersInfo( + request.WalletOperators, + request.ExecutingOperator, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot get wallet members IDs: [%w]", + err, + ) + } + + err = mft.SubmitMovingFundsCommitment( + walletPublicKeyHash, + walletMainUtxo, + walletMemberIDs, + walletMemberIndex, + targetWallets, + ) if err != nil { return nil, false, fmt.Errorf( "error while submitting moving funds commitment: [%w]", @@ -336,10 +354,63 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( return targetWallets, nil } -func (mft *MovingFundsTask) SubmitMovingFundsCommitment() ([][20]byte, error) { +func (mft *MovingFundsTask) GetWalletMembersInfo( + walletOperators []chain.Address, + executingOperator chain.Address, +) ([]uint32, uint32, error) { + walletMemberIDs := make([]uint32, 0) + walletMemberIndex := 0 + + for index, operatorAddress := range walletOperators { + operatorID, err := mft.chain.GetOperatorID(operatorAddress) + if err != nil { + return nil, 0, fmt.Errorf("failed to get operator ID: [%w]", err) + } + + // Increment the index by 1 as operator indexing starts at 1, not 0. + // This ensures the operator's position is correctly identified in the + // range [1, walletOperators.length]. + if operatorAddress == executingOperator { + walletMemberIndex = index + 1 + } + + walletMemberIDs = append(walletMemberIDs, operatorID) + } + + // The task executing operator must always be on the wallet operators list. + if walletMemberIndex == 0 { + return nil, 0, fmt.Errorf( + "task executing operator not found among wallet operators", + ) + } + + return walletMemberIDs, uint32(walletMemberIndex), nil +} + +func (mft *MovingFundsTask) SubmitMovingFundsCommitment( + walletPublicKeyHash [20]byte, + walletMainUTXO *bitcoin.UnspentTransactionOutput, + walletMembersIDs []uint32, + walletMemberIndex uint32, + targetWallets [][20]byte, +) error { + err := mft.chain.SubmitMovingFundsCommitment( + walletPublicKeyHash, + *walletMainUTXO, + walletMembersIDs, + walletMemberIndex, + targetWallets, + ) + if err != nil { + return fmt.Errorf( + "error while submitting moving funds commitment to chain: [%w]", + err, + ) + } + // TODO: After submitting the transaction wait until it has at least one // confirmation. - return nil, nil + return nil } func (mft *MovingFundsTask) ProposeMovingFunds( From 70e0cd5be2f74355305a88d44667ef9cd22b536e Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 10 Jan 2024 17:27:45 +0100 Subject: [PATCH 20/50] Added validation of moving funds proposal --- pkg/chain/ethereum/tbtc.go | 27 ++++++++++++++++++++++++++- pkg/tbtc/chain.go | 1 + pkg/tbtc/chain_test.go | 1 + pkg/tbtc/moving_funds.go | 27 +++++++++++++++++++++++++++ pkg/tbtcpg/chain.go | 1 + pkg/tbtcpg/chain_test.go | 1 + pkg/tbtcpg/moving_funds.go | 11 ++++++++++- 7 files changed, 67 insertions(+), 2 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index f3625669a5..a53fdd0634 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1750,8 +1750,33 @@ func (tc *TbtcChain) ValidateHeartbeatProposal( func (tc *TbtcChain) ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, proposal *tbtc.MovingFundsProposal, ) error { - // TODO: Implement + abiProposal := tbtcabi.WalletProposalValidatorMovingFundsProposal{ + WalletPubKeyHash: walletPublicKeyHash, + TargetWallets: proposal.TargetWallets, + MovingFundsTxFee: proposal.MovingFundsTxFee, + } + abiMainUTXO := tbtcabi.BitcoinTxUTXO3{ + TxHash: mainUTXO.Outpoint.TransactionHash, + TxOutputIndex: mainUTXO.Outpoint.OutputIndex, + TxOutputValue: uint64(mainUTXO.Value), + } + + valid, err := tc.walletProposalValidator.ValidateMovingFundsProposal( + abiProposal, + abiMainUTXO, + ) + if err != nil { + return fmt.Errorf("validation failed: [%v]", err) + } + + // Should never happen because `validateMovingFundsProposal` returns true + // or reverts (returns an error) but do the check just in case. + if !valid { + return fmt.Errorf("unexpected validation result") + } + return nil } diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index a675cf6a85..cc685ef0cc 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -364,6 +364,7 @@ type WalletProposalValidatorChain interface { // nil otherwise. ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, proposal *MovingFundsProposal, ) error } diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 363991e10f..f0effb8d61 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -890,6 +890,7 @@ func (lc *localChain) setHeartbeatProposalValidationResult( func (lc *localChain) ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, proposal *MovingFundsProposal, ) error { // TODO: Implement diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 0791647bdf..6bf294a401 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -1,6 +1,7 @@ package tbtc import ( + "fmt" "math/big" "github.com/ipfs/go-log/v2" @@ -90,8 +91,34 @@ func (mfa *movingFundsAction) execute() error { // validation rules. func ValidateMovingFundsProposal( validateProposalLogger log.StandardLogger, + walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, proposal *MovingFundsProposal, + chain interface { + // 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, + mainUTXO *bitcoin.UnspentTransactionOutput, + proposal *MovingFundsProposal, + ) error + }, ) ([]*MovingFundsRequest, error) { + validateProposalLogger.Infof("calling chain for proposal validation") + + err := chain.ValidateMovingFundsProposal( + walletPublicKeyHash, + mainUTXO, + proposal, + ) + if err != nil { + return nil, fmt.Errorf( + "moving funds proposal is invalid: [%v]", + err, + ) + } + // TODO: Implement return nil, nil } diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 14742d1fdc..85367054a5 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -145,6 +145,7 @@ type Chain interface { // nil otherwise. ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, proposal *tbtc.MovingFundsProposal, ) error } diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 1d86e01926..9c3f90ce59 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -618,6 +618,7 @@ func (lc *LocalChain) SetHeartbeatProposalValidationResult( func (lc *LocalChain) ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, proposal *tbtc.MovingFundsProposal, ) error { // TODO: Implement diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index a0d3af7d31..1b691e7a65 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -115,7 +115,10 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( proposal, err := mft.ProposeMovingFunds( taskLogger, + walletPublicKeyHash, + walletMainUtxo, targetWallets, + 0, ) if err != nil { return nil, false, fmt.Errorf( @@ -415,7 +418,10 @@ func (mft *MovingFundsTask) SubmitMovingFundsCommitment( func (mft *MovingFundsTask) ProposeMovingFunds( taskLogger log.StandardLogger, + walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, targetWallets [][20]byte, + fee int64, ) (*tbtc.MovingFundsProposal, error) { taskLogger.Infof("preparing a moving funds proposal") @@ -427,10 +433,13 @@ func (mft *MovingFundsTask) ProposeMovingFunds( taskLogger.Infof("validating the moving funds proposal") if _, err := tbtc.ValidateMovingFundsProposal( taskLogger, + walletPublicKeyHash, + mainUTXO, proposal, + mft.chain, ); err != nil { return nil, fmt.Errorf( - "failed to verify moving funds proposal: %v", + "failed to verify moving funds proposal: %w", err, ) } From 8ef8b5884d706f36085ab8a313d348456c6b53d7 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 11 Jan 2024 19:46:38 +0100 Subject: [PATCH 21/50] Added fee estimation --- pkg/chain/ethereum/tbtc.go | 35 ++++++++++++++++++++ pkg/tbtcpg/chain.go | 17 ++++++++++ pkg/tbtcpg/chain_test.go | 18 +++++++++++ pkg/tbtcpg/moving_funds.go | 65 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 133 insertions(+), 2 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index a53fdd0634..b9bc85931f 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1748,6 +1748,41 @@ func (tc *TbtcChain) ValidateHeartbeatProposal( return nil } +func (tc *TbtcChain) GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, +) { + parameters, callErr := tc.bridge.MovingFundsParameters() + if callErr != nil { + err = callErr + return + } + + txMaxTotalFee = parameters.MovingFundsTxMaxTotalFee + dustThreshold = parameters.MovingFundsDustThreshold + timeoutResetDelay = parameters.MovingFundsTimeoutResetDelay + timeout = parameters.MovingFundsTimeout + timeoutSlashingAmount = parameters.MovingFundsTimeoutSlashingAmount + timeoutNotifierRewardMultiplier = parameters.MovingFundsTimeoutNotifierRewardMultiplier + commitmentGasOffset = parameters.MovingFundsCommitmentGasOffset + sweepTxMaxTotalFee = parameters.MovedFundsSweepTxMaxTotalFee + sweepTimeout = parameters.MovedFundsSweepTimeout + sweepTimeoutSlashingAmount = parameters.MovedFundsSweepTimeoutSlashingAmount + sweepTimeoutNotifierRewardMultiplier = parameters.MovedFundsSweepTimeoutNotifierRewardMultiplier + + return +} + func (tc *TbtcChain) ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, mainUTXO *bitcoin.UnspentTransactionOutput, diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 85367054a5..58bcd10687 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -131,6 +131,23 @@ type Chain interface { proposal *tbtc.HeartbeatProposal, ) error + // GetMovingFundsParameters gets the current value of parameters relevant + // for the moving funds process. + GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, + ) + // PastMovingFundsCommitmentSubmittedEvents fetches past moving funds // commitment submitted events according to the provided filter or // unfiltered if the filter is nil. Returned events are sorted by the block diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 9c3f90ce59..7883842ad5 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -616,6 +616,24 @@ func (lc *LocalChain) SetHeartbeatProposalValidationResult( lc.heartbeatProposalValidations[proposal.Message] = result } +func (lc *LocalChain) GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, +) { + // TODO: Implement + panic("unsupported") +} + func (lc *LocalChain) ValidateMovingFundsProposal( walletPublicKeyHash [20]byte, mainUTXO *bitcoin.UnspentTransactionOutput, diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 1b691e7a65..b1923579f8 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -423,11 +423,41 @@ func (mft *MovingFundsTask) ProposeMovingFunds( targetWallets [][20]byte, fee int64, ) (*tbtc.MovingFundsProposal, error) { + if len(targetWallets) == 0 { + return nil, fmt.Errorf("target wallets list is empty") + } + taskLogger.Infof("preparing a moving funds proposal") + if fee <= 0 { + taskLogger.Infof("estimating moving funds transaction fee") + var err error + txMaxTotalFee, _, _, _, _, _, _, _, _, _, _, err := mft.chain.GetMovingFundsParameters() + if err != nil { + return nil, fmt.Errorf( + "cannot get moving funds tx max total fee: [%w]", + err, + ) + } + + estimatedFee, err := EstimateMovingFundsFee( + mft.btcChain, + len(targetWallets), + txMaxTotalFee, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot estimate moving funds transaction fee: [%w]", + err, + ) + } + + fee = estimatedFee + } + proposal := &tbtc.MovingFundsProposal{ - TargetWallets: targetWallets, - // TODO: Add fee + TargetWallets: targetWallets, + MovingFundsTxFee: big.NewInt(fee), } taskLogger.Infof("validating the moving funds proposal") @@ -450,3 +480,34 @@ func (mft *MovingFundsTask) ProposeMovingFunds( func (mft *MovingFundsTask) ActionType() tbtc.WalletActionType { return tbtc.ActionMovingFunds } + +func EstimateMovingFundsFee( + btcChain bitcoin.Chain, + targetWalletsCount int, + txMaxTotalFee uint64, +) (int64, error) { + sizeEstimator := bitcoin.NewTransactionSizeEstimator(). + AddPublicKeyHashInputs(1, true). + AddPublicKeyHashOutputs(targetWalletsCount, true) + + transactionSize, err := sizeEstimator.VirtualSize() + if err != nil { + return 0, fmt.Errorf( + "cannot estimate transaction virtual size: [%v]", + err, + ) + } + + feeEstimator := bitcoin.NewTransactionFeeEstimator(btcChain) + + totalFee, err := feeEstimator.EstimateFee(transactionSize) + if err != nil { + return 0, fmt.Errorf("cannot estimate transaction fee: [%v]", err) + } + + if uint64(totalFee) > txMaxTotalFee { + return 0, fmt.Errorf("estimated fee exceeds the maximum fee") + } + + return totalFee, nil +} From 284eb3773f443aed862b7e2b4c2ba8afeb7461d8 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 15 Jan 2024 15:12:05 +0100 Subject: [PATCH 22/50] Added functions docstrings --- pkg/tbtcpg/moving_funds.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index b1923579f8..0db21e6839 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -102,7 +102,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( return nil, false, nil } - targetWallets, commitmentSubmitted, err := mft.FindTargetWallets( + targetWallets, commitmentSubmitted, err := mft.GetTargetWallets( taskLogger, walletPublicKeyHash, walletChainData, @@ -157,7 +157,12 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( return proposal, false, nil } -func (mft *MovingFundsTask) FindTargetWallets( +// GetTargetWallets returns a list of target wallets for the moving funds +// procedure. If the source wallet has not submitted moving funds commitment yet +// a new list of target wallets is prepared. If the source wallet has already +// submitted the commitment, the returned target wallet list is prepared based +// on the submitted commitment event. +func (mft *MovingFundsTask) GetTargetWallets( taskLogger log.StandardLogger, sourceWalletPublicKeyHash [20]byte, walletChainData *tbtc.WalletChainData, @@ -357,6 +362,9 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( return targetWallets, nil } +// GetWalletMembersInfo returns the wallet member IDs based on the provided +// wallet operator addresses. Additionally, it returns the position of the +// moving funds task execution operator on the list. func (mft *MovingFundsTask) GetWalletMembersInfo( walletOperators []chain.Address, executingOperator chain.Address, @@ -390,6 +398,8 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( return walletMemberIDs, uint32(walletMemberIndex), nil } +// SubmitMovingFundsCommitment submits the moving funds commitment and waits +// until the transaction has at least one confirmation. func (mft *MovingFundsTask) SubmitMovingFundsCommitment( walletPublicKeyHash [20]byte, walletMainUTXO *bitcoin.UnspentTransactionOutput, @@ -416,6 +426,7 @@ func (mft *MovingFundsTask) SubmitMovingFundsCommitment( return nil } +// ProposeMovingFunds returns a moving funds proposal. func (mft *MovingFundsTask) ProposeMovingFunds( taskLogger log.StandardLogger, walletPublicKeyHash [20]byte, @@ -481,6 +492,8 @@ func (mft *MovingFundsTask) ActionType() tbtc.WalletActionType { return tbtc.ActionMovingFunds } +// EstimateMovingFundsFee estimates fee for the moving funds transaction that +// moves funds from the source wallet to target wallets. func EstimateMovingFundsFee( btcChain bitcoin.Chain, targetWalletsCount int, From 44a59f5238b0f0de5b77a0c03789a3e7a780bc65 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 18 Jan 2024 10:23:58 +0100 Subject: [PATCH 23/50] Added unit tests for finding target wallets --- pkg/tbtcpg/chain_test.go | 178 ++++++++++++-- pkg/tbtcpg/moving_funds.go | 93 ++++++-- pkg/tbtcpg/moving_funds_test.go | 405 ++++++++++++++++++++++++++++++++ 3 files changed, 631 insertions(+), 45 deletions(-) create mode 100644 pkg/tbtcpg/moving_funds_test.go diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 7883842ad5..254a654c14 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -33,34 +33,49 @@ type redemptionParameters = struct { timeoutNotifierRewardMultiplier uint32 } +type walletParameters = struct { + creationPeriod uint32 + creationMinBtcBalance uint64 + creationMaxBtcBalance uint64 + closureMinBtcBalance uint64 + maxAge uint32 + maxBtcTransfer uint64 + closingPeriod uint32 +} + type LocalChain struct { mutex sync.Mutex - depositRequests map[[32]byte]*tbtc.DepositChainRequest - pastDepositRevealedEvents map[[32]byte][]*tbtc.DepositRevealedEvent - pastNewWalletRegisteredEvents map[[32]byte][]*tbtc.NewWalletRegisteredEvent - depositParameters depositParameters - depositSweepProposalValidations map[[32]byte]bool - redemptionParameters redemptionParameters - redemptionRequestMinAge uint32 - blockCounter chain.BlockCounter - pastRedemptionRequestedEvents map[[32]byte][]*tbtc.RedemptionRequestedEvent - averageBlockTime time.Duration - pendingRedemptionRequests map[[32]byte]*tbtc.RedemptionRequest - redemptionProposalValidations map[[32]byte]bool - heartbeatProposalValidations map[[16]byte]bool + depositRequests map[[32]byte]*tbtc.DepositChainRequest + pastDepositRevealedEvents map[[32]byte][]*tbtc.DepositRevealedEvent + pastNewWalletRegisteredEvents map[[32]byte][]*tbtc.NewWalletRegisteredEvent + depositParameters depositParameters + depositSweepProposalValidations map[[32]byte]bool + redemptionParameters redemptionParameters + redemptionRequestMinAge uint32 + walletParameters walletParameters + walletChainData map[[20]byte]*tbtc.WalletChainData + blockCounter chain.BlockCounter + pastRedemptionRequestedEvents map[[32]byte][]*tbtc.RedemptionRequestedEvent + averageBlockTime time.Duration + pendingRedemptionRequests map[[32]byte]*tbtc.RedemptionRequest + redemptionProposalValidations map[[32]byte]bool + pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent + heartbeatProposalValidations map[[16]byte]bool } func NewLocalChain() *LocalChain { return &LocalChain{ - depositRequests: make(map[[32]byte]*tbtc.DepositChainRequest), - pastDepositRevealedEvents: make(map[[32]byte][]*tbtc.DepositRevealedEvent), - pastNewWalletRegisteredEvents: make(map[[32]byte][]*tbtc.NewWalletRegisteredEvent), - depositSweepProposalValidations: make(map[[32]byte]bool), - pastRedemptionRequestedEvents: make(map[[32]byte][]*tbtc.RedemptionRequestedEvent), - pendingRedemptionRequests: make(map[[32]byte]*tbtc.RedemptionRequest), - redemptionProposalValidations: make(map[[32]byte]bool), - heartbeatProposalValidations: make(map[[16]byte]bool), + depositRequests: make(map[[32]byte]*tbtc.DepositChainRequest), + pastDepositRevealedEvents: make(map[[32]byte][]*tbtc.DepositRevealedEvent), + pastNewWalletRegisteredEvents: make(map[[32]byte][]*tbtc.NewWalletRegisteredEvent), + depositSweepProposalValidations: make(map[[32]byte]bool), + pastRedemptionRequestedEvents: make(map[[32]byte][]*tbtc.RedemptionRequestedEvent), + walletChainData: make(map[[20]byte]*tbtc.WalletChainData), + pendingRedemptionRequests: make(map[[32]byte]*tbtc.RedemptionRequest), + redemptionProposalValidations: make(map[[32]byte]bool), + pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent), + heartbeatProposalValidations: make(map[[16]byte]bool), } } @@ -315,6 +330,32 @@ func buildPastRedemptionRequestedEventsKey( return sha256.Sum256(buffer.Bytes()), nil } +func buildPastMovingFundsCommitmentSubmittedEventsKey( + filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, +) ([32]byte, error) { + if filter == nil { + return [32]byte{}, nil + } + + var buffer bytes.Buffer + + startBlock := make([]byte, 8) + binary.BigEndian.PutUint64(startBlock, filter.StartBlock) + buffer.Write(startBlock) + + if filter.EndBlock != nil { + endBlock := make([]byte, 8) + binary.BigEndian.PutUint64(startBlock, *filter.EndBlock) + buffer.Write(endBlock) + } + + for _, walletPublicKeyHash := range filter.WalletPublicKeyHash { + buffer.Write(walletPublicKeyHash[:]) + } + + return sha256.Sum256(buffer.Bytes()), nil +} + func (lc *LocalChain) BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int { depositKeyBytes := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) @@ -728,7 +769,26 @@ func (lc *LocalChain) GetWallet(walletPublicKeyHash [20]byte) ( *tbtc.WalletChainData, error, ) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + data, ok := lc.walletChainData[walletPublicKeyHash] + if !ok { + fmt.Println("Not found") + return nil, fmt.Errorf("wallet chain data not found") + } + + return data, nil +} + +func (lc *LocalChain) SetWallet( + walletPublicKeyHash [20]byte, + data *tbtc.WalletChainData, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.walletChainData[walletPublicKeyHash] = data } func (lc *LocalChain) GetWalletParameters() ( @@ -741,7 +801,40 @@ func (lc *LocalChain) GetWalletParameters() ( closingPeriod uint32, err error, ) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.walletParameters.creationPeriod, + lc.walletParameters.creationMinBtcBalance, + lc.walletParameters.creationMaxBtcBalance, + lc.walletParameters.closureMinBtcBalance, + lc.walletParameters.maxAge, + lc.walletParameters.maxBtcTransfer, + lc.walletParameters.closingPeriod, + nil +} + +func (lc *LocalChain) SetWalletParameters( + creationPeriod uint32, + creationMinBtcBalance uint64, + creationMaxBtcBalance uint64, + closureMinBtcBalance uint64, + maxAge uint32, + maxBtcTransfer uint64, + closingPeriod uint32, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.walletParameters = walletParameters{ + creationPeriod: creationPeriod, + creationMinBtcBalance: creationMinBtcBalance, + creationMaxBtcBalance: creationMaxBtcBalance, + closureMinBtcBalance: closureMinBtcBalance, + maxAge: maxAge, + maxBtcTransfer: maxBtcTransfer, + closingPeriod: closingPeriod, + } } func (lc *LocalChain) GetLiveWalletsCount() (uint32, error) { @@ -752,10 +845,47 @@ func (lc *LocalChain) ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOu panic("unsupported") } +func (lc *LocalChain) AddPastMovingFundsCommitmentSubmittedEvent( + filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, + event *tbtc.MovingFundsCommitmentSubmittedEvent, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastMovingFundsCommitmentSubmittedEventsKey(filter) + if err != nil { + return err + } + + if _, ok := lc.pastMovingFundsCommitmentSubmittedEvents[eventsKey]; !ok { + lc.pastMovingFundsCommitmentSubmittedEvents[eventsKey] = []*tbtc.MovingFundsCommitmentSubmittedEvent{} + } + + lc.pastMovingFundsCommitmentSubmittedEvents[eventsKey] = append( + lc.pastMovingFundsCommitmentSubmittedEvents[eventsKey], + event, + ) + + return nil +} + func (lc *LocalChain) PastMovingFundsCommitmentSubmittedEvents( filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, ) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastMovingFundsCommitmentSubmittedEventsKey(filter) + if err != nil { + return nil, err + } + + events, ok := lc.pastMovingFundsCommitmentSubmittedEvents[eventsKey] + if !ok { + return nil, fmt.Errorf("no events for given filter") + } + + return events, nil } func (lc *LocalChain) SubmitMovingFundsCommitment( diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 0db21e6839..e4778c3752 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -1,18 +1,50 @@ package tbtcpg import ( + "bytes" "fmt" "math/big" "sort" + "time" - "go.uber.org/zap" - + "github.com/ethereum/go-ethereum/crypto" "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" + "go.uber.org/zap" ) +var ( + // ErrMaxBtcTransferZero is the error returned when wallet max BTC transfer + // parameter is zero. + ErrMaxBtcTransferZero = fmt.Errorf( + "wallet max BTC transfer must be positive", + ) + + // ErrNotEnoughTargetWallets is the error returned when the number of + // gathered target wallets does not match the required target wallets count. + ErrNotEnoughTargetWallets = fmt.Errorf( + "not enough target wallets", + ) + + // ErrWrongCommitmentHash is the error returned when the hash calculated + // from retrieved target wallets does not match the committed hash. + ErrWrongCommitmentHash = fmt.Errorf( + "target wallets hash must match commitment hash", + ) + + // ErrTargetWalletNotLive is the error returned when a target wallet is not + // in the Live state. + ErrTargetWalletNotLive = fmt.Errorf( + "target wallet is not live", + ) +) + +// MovingFundsCommitmentLookBackPeriod is the look-back period used when +// searching for submitted moving funds commitment events. +const MovingFundsCommitmentLookBackPeriod = 30 * 24 * time.Hour // 1 month + // MovingFundsTask is a task that may produce a moving funds proposal. type MovingFundsTask struct { chain Chain @@ -102,10 +134,13 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( return nil, false, nil } - targetWallets, commitmentSubmitted, err := mft.GetTargetWallets( + targetWalletsCommitmentHash := + walletChainData.MovingFundsTargetWalletsCommitmentHash + + targetWallets, commitmentSubmitted, err := mft.FindTargetWallets( taskLogger, walletPublicKeyHash, - walletChainData, + targetWalletsCommitmentHash, uint64(walletBalance), liveWalletsCount, ) @@ -157,33 +192,36 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( return proposal, false, nil } -// GetTargetWallets returns a list of target wallets for the moving funds +// FindTargetWallets returns a list of target wallets for the moving funds // procedure. If the source wallet has not submitted moving funds commitment yet // a new list of target wallets is prepared. If the source wallet has already // submitted the commitment, the returned target wallet list is prepared based // on the submitted commitment event. -func (mft *MovingFundsTask) GetTargetWallets( +func (mft *MovingFundsTask) FindTargetWallets( taskLogger log.StandardLogger, sourceWalletPublicKeyHash [20]byte, - walletChainData *tbtc.WalletChainData, + targetWalletsCommitmentHash [32]byte, walletBalance uint64, liveWalletsCount uint32, ) ([][20]byte, bool, error) { - if walletChainData.MovingFundsTargetWalletsCommitmentHash == [32]byte{} { + if targetWalletsCommitmentHash == [32]byte{} { targetWallets, err := mft.findNewTargetWallets( taskLogger, sourceWalletPublicKeyHash, walletBalance, liveWalletsCount, ) + return targetWallets, false, err - } + } else { + targetWallets, err := mft.retrieveCommittedTargetWallets( + taskLogger, + sourceWalletPublicKeyHash, + targetWalletsCommitmentHash, + ) - targetWallets, err := mft.retrieveCommittedTargetWallets( - taskLogger, - sourceWalletPublicKeyHash, - ) - return targetWallets, true, err + return targetWallets, true, err + } } func (mft *MovingFundsTask) findNewTargetWallets( @@ -202,9 +240,7 @@ func (mft *MovingFundsTask) findNewTargetWallets( } if walletMaxBtcTransfer == 0 { - return nil, fmt.Errorf( - "wallet max BTC transfer must be positive: [%w]", err, - ) + return nil, ErrMaxBtcTransferZero } ceilingDivide := func(x, y uint64) uint64 { @@ -268,7 +304,8 @@ func (mft *MovingFundsTask) findNewTargetWallets( if len(targetWallets) != int(targetWalletsCount) { return nil, fmt.Errorf( - "failed to get enough target wallets: required [%v]; gathered [%v]", + "%w: required [%v] target wallets; gathered [%v]", + ErrNotEnoughTargetWallets, targetWalletsCount, len(targetWallets), ) @@ -290,6 +327,7 @@ func (mft *MovingFundsTask) findNewTargetWallets( func (mft *MovingFundsTask) retrieveCommittedTargetWallets( taskLogger log.StandardLogger, sourceWalletPublicKeyHash [20]byte, + targetWalletsCommitmentHash [32]byte, ) ([][20]byte, error) { taskLogger.Infof( "commitment already submitted; retrieving committed target wallets", @@ -311,8 +349,7 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( ) } - // Look back one month when searching for the commitment event. - filterLookBackSeconds := uint64(30 * 24 * 60 * 60) + filterLookBackSeconds := uint64(MovingFundsCommitmentLookBackPeriod.Seconds()) filterLookBackBlocks := filterLookBackSeconds / uint64(mft.chain.AverageBlockTime().Seconds()) filterStartBlock := currentBlockNumber - filterLookBackBlocks @@ -353,12 +390,26 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( if targetWalletChainData.State != tbtc.StateLive { return nil, fmt.Errorf( - "target wallet [0x%x] is not Live", + "%w: [0x%x]", + ErrTargetWalletNotLive, targetWallet, ) } } + // Just in case check if the hash of the target wallets matches the moving + // funds target wallets commitment hash. + packedWallets := []byte{} + for _, wallet := range targetWallets { + packedWallets = append(packedWallets, wallet[:]...) + } + + calculatedHash := crypto.Keccak256(packedWallets) + + if !bytes.Equal(calculatedHash, targetWalletsCommitmentHash[:]) { + return nil, ErrWrongCommitmentHash + } + return targetWallets, nil } diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go new file mode 100644 index 0000000000..4b783ee496 --- /dev/null +++ b/pkg/tbtcpg/moving_funds_test.go @@ -0,0 +1,405 @@ +package tbtcpg_test + +import ( + "encoding/hex" + "reflect" + "testing" + "time" + + "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/tbtc" + "github.com/keep-network/keep-core/pkg/tbtcpg" +) + +func TestMovingFundsAction_FindTargetWallets_CommitmentNotSubmittedYet(t *testing.T) { + var tests = map[string]struct { + sourceWalletPublicKeyHash [20]byte + walletBalance uint64 + walletMaxBtcTransfer uint64 + registeredWallets []walletInfo + liveWalletsCount uint32 + expectedTargetWallets [][20]byte + expectedError error + }{ + "success scenario": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + walletBalance: 2000000, + walletMaxBtcTransfer: 300000, + registeredWallets: []walletInfo{ + { + publicKeyHash: hexToByte20( + "92a6ec889a8fa34f731e639edede4c75e184307c", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "fdfa28e238734271f5e0d4f53d3843ae6cc09b24", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "840dac51a6346e9372efbdc5d3503ed9fd32abdf", + ), + state: tbtc.StateMovingFunds, + }, + { + publicKeyHash: hexToByte20( + "3091d288521caec06ea912eacfd733edc5a36d6e", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "c7302d75072d78be94eb8d36c4b77583c7abb06e", + ), + state: tbtc.StateLive, + }, + }, + liveWalletsCount: 4, + expectedTargetWallets: [][20]byte{ + // The target wallets list should include all the Live wallets + // and the wallets should be sorted according to their numerical + // representation. + hexToByte20( + "3091d288521caec06ea912eacfd733edc5a36d6e", + ), + hexToByte20( + "92a6ec889a8fa34f731e639edede4c75e184307c", + ), + hexToByte20( + "c7302d75072d78be94eb8d36c4b77583c7abb06e", + ), + hexToByte20( + "fdfa28e238734271f5e0d4f53d3843ae6cc09b24", + ), + }, + expectedError: nil, + }, + "wallet max BTC transfer is zero": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + walletBalance: 10000, + walletMaxBtcTransfer: 0, // Set to zero. + registeredWallets: []walletInfo{}, + liveWalletsCount: 4, + expectedTargetWallets: nil, + expectedError: tbtcpg.ErrMaxBtcTransferZero, + }, + "not enough live wallets": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + walletBalance: 2000000, + walletMaxBtcTransfer: 300000, + // Simulate there should be two Live wallets, but set only one Live + // wallet. This could only happen if one of the target wallets + // changed its state between getting the live wallets count and + // getting wallets chain data. + registeredWallets: []walletInfo{ + { + publicKeyHash: hexToByte20( + "92a6ec889a8fa34f731e639edede4c75e184307c", + ), + state: tbtc.StateClosing, + }, + { + publicKeyHash: hexToByte20( + "fdfa28e238734271f5e0d4f53d3843ae6cc09b24", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "840dac51a6346e9372efbdc5d3503ed9fd32abdf", + ), + state: tbtc.StateMovingFunds, + }, + }, + liveWalletsCount: 2, + expectedTargetWallets: nil, + expectedError: tbtcpg.ErrNotEnoughTargetWallets, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + tbtcChain.SetWalletParameters( + 0, + 0, + 0, + 0, + 0, + test.walletMaxBtcTransfer, + 0, + ) + + for _, walletInfo := range test.registeredWallets { + tbtcChain.AddPastNewWalletRegisteredEvent( + nil, + &tbtc.NewWalletRegisteredEvent{ + WalletPublicKeyHash: walletInfo.publicKeyHash, + }, + ) + tbtcChain.SetWallet( + walletInfo.publicKeyHash, + &tbtc.WalletChainData{State: walletInfo.state}, + ) + } + + task := tbtcpg.NewMovingFundsTask(tbtcChain, nil) + + // Always simulate the moving funds commitment has not been + // submitted yet. + targetWalletsCommitmentHash := [32]byte{} + + targetWallets, alreadySubmitted, err := task.FindTargetWallets( + &testutils.MockLogger{}, + test.sourceWalletPublicKeyHash, + targetWalletsCommitmentHash, + test.walletBalance, + test.liveWalletsCount, + ) + + if !reflect.DeepEqual(test.expectedTargetWallets, targetWallets) { + t.Errorf( + "unexpected target wallets\nexpected: %v\nactual: %v", + test.expectedTargetWallets, + targetWallets, + ) + } + + // Returned value for the already submitted commitment should + // always be false. + expectedAlreadySubmitted := false + testutils.AssertBoolsEqual( + t, + "already submitted flag", + expectedAlreadySubmitted, + alreadySubmitted, + ) + + testutils.AssertAnyErrorInChainMatchesTarget( + t, + test.expectedError, + err, + ) + }) + } +} + +func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testing.T) { + var tests = map[string]struct { + sourceWalletPublicKeyHash [20]byte + targetWalletsCommitmentHash [32]byte + targetWallets []walletInfo + currentBlock uint64 + averageBlockTime time.Duration + expectedTargetWallets [][20]byte + expectedError error + }{ + "success scenario": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + targetWalletsCommitmentHash: hexToByte32( + "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + ), + targetWallets: []walletInfo{ + { + publicKeyHash: hexToByte20( + "92a6ec889a8fa34f731e639edede4c75e184307c", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "c7302d75072d78be94eb8d36c4b77583c7abb06e", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "fdfa28e238734271f5e0d4f53d3843ae6cc09b24", + ), + state: tbtc.StateLive, + }, + }, + currentBlock: 1000000, + averageBlockTime: 10 * time.Second, + expectedTargetWallets: [][20]byte{ + hexToByte20("92a6ec889a8fa34f731e639edede4c75e184307c"), + hexToByte20("c7302d75072d78be94eb8d36c4b77583c7abb06e"), + hexToByte20("fdfa28e238734271f5e0d4f53d3843ae6cc09b24"), + }, + expectedError: nil, + }, + "target wallet is not live": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + targetWalletsCommitmentHash: hexToByte32( + "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + ), + targetWallets: []walletInfo{ + { + publicKeyHash: hexToByte20( + "92a6ec889a8fa34f731e639edede4c75e184307c", + ), + state: tbtc.StateLive, + }, + { + publicKeyHash: hexToByte20( + "c7302d75072d78be94eb8d36c4b77583c7abb06e", + ), + state: tbtc.StateTerminated, // wrong state + }, + { + publicKeyHash: hexToByte20( + "fdfa28e238734271f5e0d4f53d3843ae6cc09b24", + ), + state: tbtc.StateLive, + }, + }, + currentBlock: 1000000, + averageBlockTime: 10 * time.Second, + expectedTargetWallets: nil, + expectedError: tbtcpg.ErrTargetWalletNotLive, + }, + "target wallet commitment hash mismatch": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + targetWalletsCommitmentHash: hexToByte32( + "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + ), + targetWallets: []walletInfo{ + { // Use only one target wallet. + publicKeyHash: hexToByte20( + "92a6ec889a8fa34f731e639edede4c75e184307c", + ), + state: tbtc.StateLive, + }, + }, + currentBlock: 1000000, + averageBlockTime: 10 * time.Second, + expectedTargetWallets: nil, + expectedError: tbtcpg.ErrWrongCommitmentHash, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + + tbtcChain.SetAverageBlockTime(test.averageBlockTime) + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(test.currentBlock) + tbtcChain.SetBlockCounter(blockCounter) + + startBlock := test.currentBlock - uint64( + tbtcpg.MovingFundsCommitmentLookBackPeriod.Seconds(), + )/uint64(test.averageBlockTime.Seconds()) + + targetWallets := [][20]byte{} + for _, walletInfo := range test.targetWallets { + targetWallets = append(targetWallets, walletInfo.publicKeyHash) + tbtcChain.SetWallet( + walletInfo.publicKeyHash, + &tbtc.WalletChainData{State: walletInfo.state}, + ) + } + + err := tbtcChain.AddPastMovingFundsCommitmentSubmittedEvent( + &tbtc.MovingFundsCommitmentSubmittedEventFilter{ + StartBlock: startBlock, + WalletPublicKeyHash: [][20]byte{ + test.sourceWalletPublicKeyHash, + }, + }, + &tbtc.MovingFundsCommitmentSubmittedEvent{ + TargetWallets: targetWallets, + }, + ) + if err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewMovingFundsTask(tbtcChain, nil) + + // Live wallets count and wallet's balance don't matter, as we are + // retrieving target wallets from an already submitted commitment. + walletBalance := uint64(2000000) + liveWalletsCount := uint32(5) + + targetWallets, alreadySubmitted, err := task.FindTargetWallets( + &testutils.MockLogger{}, + test.sourceWalletPublicKeyHash, + test.targetWalletsCommitmentHash, + walletBalance, + liveWalletsCount, + ) + + if !reflect.DeepEqual(test.expectedTargetWallets, targetWallets) { + t.Errorf( + "unexpected target wallets\nexpected: %v\nactual: %v", + test.expectedTargetWallets, + targetWallets, + ) + } + + // Returned value for the already submitted commitment should + // always be true. + expectedAlreadySubmitted := true + testutils.AssertBoolsEqual( + t, + "already submitted flag", + expectedAlreadySubmitted, + alreadySubmitted, + ) + + testutils.AssertAnyErrorInChainMatchesTarget( + t, + test.expectedError, + err, + ) + }) + } +} + +type walletInfo struct { + publicKeyHash [20]byte + state tbtc.WalletState +} + +func hexToByte20(hexStr string) [20]byte { + if len(hexStr) != 40 { + panic("hex string length incorrect") + } + decoded, err := hex.DecodeString(hexStr) + if err != nil { + panic(err) + } + var result [20]byte + copy(result[:], decoded) + return result +} + +func hexToByte32(hexStr string) [32]byte { + if len(hexStr) != 64 { + panic("hex string length incorrect") + } + decoded, err := hex.DecodeString(hexStr) + if err != nil { + panic(err) + } + var result [32]byte + copy(result[:], decoded) + return result +} From 4ab2c94c7b5051a522fbcee18ac866a80023c005 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 19 Jan 2024 12:34:07 +0100 Subject: [PATCH 24/50] Added unit tests for getting wallet member info --- pkg/tbtcpg/chain_test.go | 29 ++++++++++- pkg/tbtcpg/moving_funds.go | 10 ++-- pkg/tbtcpg/moving_funds_test.go | 85 +++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 254a654c14..551db49d14 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -62,6 +62,7 @@ type LocalChain struct { redemptionProposalValidations map[[32]byte]bool pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent heartbeatProposalValidations map[[16]byte]bool + operatorIDs map[chain.Address]uint32 } func NewLocalChain() *LocalChain { @@ -76,6 +77,7 @@ func NewLocalChain() *LocalChain { redemptionProposalValidations: make(map[[32]byte]bool), pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent), heartbeatProposalValidations: make(map[[16]byte]bool), + operatorIDs: make(map[chain.Address]uint32), } } @@ -752,10 +754,35 @@ func (lc *LocalChain) AverageBlockTime() time.Duration { return lc.averageBlockTime } +func (lc *LocalChain) SetOperatorID( + operatorAddress chain.Address, + operatorID chain.OperatorID, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + _, ok := lc.operatorIDs[operatorAddress] + if ok { + return fmt.Errorf("operator already inserted") + } + + lc.operatorIDs[operatorAddress] = operatorID + + return nil +} + func (lc *LocalChain) GetOperatorID( operatorAddress chain.Address, ) (chain.OperatorID, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + operatorID, ok := lc.operatorIDs[operatorAddress] + if !ok { + return 0, fmt.Errorf("operator not found") + } + + return operatorID, nil } func (lc *LocalChain) SetAverageBlockTime(averageBlockTime time.Duration) { diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index e4778c3752..a50ac87f18 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -39,6 +39,12 @@ var ( ErrTargetWalletNotLive = fmt.Errorf( "target wallet is not live", ) + + // ErrNoExecutingOperator is the error returned when the task executing + // operator is not found among the wallet operator IDs. + ErrNoExecutingOperator = fmt.Errorf( + "task executing operator not found among wallet operators", + ) ) // MovingFundsCommitmentLookBackPeriod is the look-back period used when @@ -441,9 +447,7 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( // The task executing operator must always be on the wallet operators list. if walletMemberIndex == 0 { - return nil, 0, fmt.Errorf( - "task executing operator not found among wallet operators", - ) + return nil, 0, ErrNoExecutingOperator } return walletMemberIDs, uint32(walletMemberIndex), nil diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index 4b783ee496..0080779ce4 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/tbtcpg" ) @@ -373,11 +374,95 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi } } +func TestMovingFundsAction_GetWalletMembersInfo(t *testing.T) { + var tests = map[string]struct { + walletOperators []operatorInfo + executingOperator chain.Address + expectedMemberIDs []uint32 + expectedOperatorPosition uint32 + expectedError error + }{ + "success case": { + walletOperators: []operatorInfo{ + {"5df232b0348928793658dd05dfc6b05a59d11ae8", 3}, + {"dcc895d32b74b34cef2baa6546884fcda65da1e9", 1}, + {"28759deda2ea33bd72f68ea2e8f60cd670c2549f", 2}, + {"f7891d42f3c61a49e0aed1e31b151877c0905cf7", 4}, + }, + executingOperator: "28759deda2ea33bd72f68ea2e8f60cd670c2549f", + expectedMemberIDs: []uint32{3, 1, 2, 4}, + expectedOperatorPosition: 3, + expectedError: nil, + }, + "executing operator not among operators": { + walletOperators: []operatorInfo{ + {"5df232b0348928793658dd05dfc6b05a59d11ae8", 2}, + {"dcc895d32b74b34cef2baa6546884fcda65da1e9", 1}, + }, + executingOperator: "28759deda2ea33bd72f68ea2e8f60cd670c2549f", + expectedMemberIDs: nil, + expectedOperatorPosition: 0, + expectedError: tbtcpg.ErrNoExecutingOperator, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + + task := tbtcpg.NewMovingFundsTask(tbtcChain, nil) + + walletOperators := []chain.Address{} + for _, operatorInfo := range test.walletOperators { + err := tbtcChain.SetOperatorID( + operatorInfo.Address, + operatorInfo.OperatorID, + ) + if err != nil { + t.Fatal(err) + } + walletOperators = append(walletOperators, operatorInfo.Address) + } + + memberIDs, operatorPosition, err := task.GetWalletMembersInfo( + walletOperators, + test.executingOperator, + ) + + if !reflect.DeepEqual(test.expectedMemberIDs, memberIDs) { + t.Errorf( + "unexpected memberIDs\nexpected: %v\nactual: %v", + test.expectedMemberIDs, + memberIDs, + ) + } + + testutils.AssertUintsEqual( + t, + "operator position", + uint64(test.expectedOperatorPosition), + uint64(operatorPosition), + ) + + testutils.AssertAnyErrorInChainMatchesTarget( + t, + test.expectedError, + err, + ) + }) + } +} + type walletInfo struct { publicKeyHash [20]byte state tbtc.WalletState } +type operatorInfo struct { + chain.Address + chain.OperatorID +} + func hexToByte20(hexStr string) [20]byte { if len(hexStr) != 40 { panic("hex string length incorrect") From d0074435d59af86e964f284662227ccbbe079679 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 22 Jan 2024 13:17:02 +0100 Subject: [PATCH 25/50] Added commitment transaction confirmation check --- pkg/tbtcpg/moving_funds.go | 46 +++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index a50ac87f18..0b42dbfe2f 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" @@ -454,7 +455,7 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( } // SubmitMovingFundsCommitment submits the moving funds commitment and waits -// until the transaction has at least one confirmation. +// until the transaction has entered the Ethereum blockchain. func (mft *MovingFundsTask) SubmitMovingFundsCommitment( walletPublicKeyHash [20]byte, walletMainUTXO *bitcoin.UnspentTransactionOutput, @@ -476,8 +477,47 @@ func (mft *MovingFundsTask) SubmitMovingFundsCommitment( ) } - // TODO: After submitting the transaction wait until it has at least one - // confirmation. + blockCounter, err := mft.chain.BlockCounter() + if err != nil { + return fmt.Errorf("error getting block counter [%w]", err) + } + + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return fmt.Errorf("error getting current block [%w]", err) + } + + // To verify the commitment transaction has entered the Ethereum blockchain + // check that the commitment hash is not zero. + stateCheck := func() (bool, error) { + walletData, err := mft.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return false, err + } + + return walletData.MovingFundsTargetWalletsCommitmentHash != [32]byte{}, nil + } + + // Wait `5` blocks since the current block and perform the transaction state + // check. If the transaction has not entered the blockchain, consider it an + // error. + result, err := ethereum.WaitForBlockConfirmations( + blockCounter, + currentBlock, + 5, + stateCheck, + ) + if err != nil { + return fmt.Errorf( + "error while waiting for transaction confirmation [%w]", + err, + ) + } + + if !result { + return fmt.Errorf("transaction has not been confirmed") + } + return nil } From da7d7454c9fb7dbf342e9e3477416e5ea780d8ca Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 22 Jan 2024 18:53:10 +0100 Subject: [PATCH 26/50] Removed unnecessary functions --- pkg/tbtc/chain_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index b7ea2182ac..de977b8c5c 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -585,12 +585,6 @@ func (lc *localChain) GetPendingRedemptionRequest( return request, true, nil } -func (lc *localChain) PastNewWalletRegisteredEvents( - filter *NewWalletRegisteredEventFilter, -) ([]*NewWalletRegisteredEvent, error) { - panic("not supported") -} - func (lc *localChain) setPendingRedemptionRequest( walletPublicKeyHash [20]byte, request *RedemptionRequest, @@ -721,10 +715,6 @@ func (lc *localChain) GetWalletParameters() ( panic("unsupported") } -func (lc *localChain) GetLiveWalletsCount() (uint32, error) { - panic("unsupported") -} - func (lc *localChain) ValidateDepositSweepProposal( walletPublicKeyHash [20]byte, proposal *DepositSweepProposal, @@ -929,7 +919,6 @@ func (lc *localChain) ValidateMovingFundsProposal( mainUTXO *bitcoin.UnspentTransactionOutput, proposal *MovingFundsProposal, ) error { - // TODO: Implement panic("unsupported") } From 0ee592cb32f5d82e31d1b9bd12a841b0c4ded370 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 23 Jan 2024 19:08:09 +0100 Subject: [PATCH 27/50] Added unit tests for submitting moving funds commitment --- pkg/tbtcpg/chain_test.go | 47 +++++++---- pkg/tbtcpg/moving_funds.go | 8 +- pkg/tbtcpg/moving_funds_test.go | 133 ++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 14 deletions(-) diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 551db49d14..534dedd3b4 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -43,6 +43,14 @@ type walletParameters = struct { closingPeriod uint32 } +type movingFundsCommitmentSubmission struct { + WalletPublicKeyHash [20]byte + WalletMainUtxo *bitcoin.UnspentTransactionOutput + WalletMembersIDs []uint32 + WalletMemberIndex uint32 + TargetWallets [][20]byte +} + type LocalChain struct { mutex sync.Mutex @@ -63,6 +71,7 @@ type LocalChain struct { pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent heartbeatProposalValidations map[[16]byte]bool operatorIDs map[chain.Address]uint32 + movingFundsCommitmentSubmissions []*movingFundsCommitmentSubmission } func NewLocalChain() *LocalChain { @@ -78,6 +87,7 @@ func NewLocalChain() *LocalChain { pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent), heartbeatProposalValidations: make(map[[16]byte]bool), operatorIDs: make(map[chain.Address]uint32), + movingFundsCommitmentSubmissions: make([]*movingFundsCommitmentSubmission, 0), } } @@ -673,7 +683,6 @@ func (lc *LocalChain) GetMovingFundsParameters() ( sweepTimeoutNotifierRewardMultiplier uint32, err error, ) { - // TODO: Implement panic("unsupported") } @@ -682,15 +691,6 @@ func (lc *LocalChain) ValidateMovingFundsProposal( mainUTXO *bitcoin.UnspentTransactionOutput, proposal *tbtc.MovingFundsProposal, ) error { - // TODO: Implement - panic("unsupported") -} - -func (lc *LocalChain) SetMovingFundsProposalValidationResult( - proposal *tbtc.MovingFundsProposal, - result bool, -) { - // TODO: Implement panic("unsupported") } @@ -917,12 +917,33 @@ func (lc *LocalChain) PastMovingFundsCommitmentSubmittedEvents( func (lc *LocalChain) SubmitMovingFundsCommitment( walletPublicKeyHash [20]byte, - walletMainUTXO bitcoin.UnspentTransactionOutput, + walletMainUtxo bitcoin.UnspentTransactionOutput, walletMembersIDs []uint32, walletMemberIndex uint32, targetWallets [][20]byte, ) error { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.movingFundsCommitmentSubmissions = append( + lc.movingFundsCommitmentSubmissions, + &movingFundsCommitmentSubmission{ + WalletPublicKeyHash: walletPublicKeyHash, + WalletMainUtxo: &walletMainUtxo, + WalletMembersIDs: walletMembersIDs, + WalletMemberIndex: walletMemberIndex, + TargetWallets: targetWallets, + }, + ) + + return nil +} + +func (lc *LocalChain) GetMovingFundsSubmissions() []*movingFundsCommitmentSubmission { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.movingFundsCommitmentSubmissions } type MockBlockCounter struct { @@ -935,7 +956,7 @@ func NewMockBlockCounter() *MockBlockCounter { } func (mbc *MockBlockCounter) WaitForBlockHeight(blockNumber uint64) error { - panic("unsupported") + return nil } func (mbc *MockBlockCounter) BlockHeightWaiter(blockNumber uint64) ( diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 0b42dbfe2f..1d9695632c 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -46,6 +46,12 @@ var ( ErrNoExecutingOperator = fmt.Errorf( "task executing operator not found among wallet operators", ) + + // ErrTransactionNotIncluded is the error returned when the commitment + // submission transaction was not included in the Ethereum blockchain. + ErrTransactionNotIncluded = fmt.Errorf( + "transaction not included in blockchain", + ) ) // MovingFundsCommitmentLookBackPeriod is the look-back period used when @@ -515,7 +521,7 @@ func (mft *MovingFundsTask) SubmitMovingFundsCommitment( } if !result { - return fmt.Errorf("transaction has not been confirmed") + return ErrTransactionNotIncluded } return nil diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index 0080779ce4..f2fbe02899 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -6,7 +6,10 @@ import ( "testing" "time" + "github.com/go-test/deep" + "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/tbtcpg" @@ -453,6 +456,136 @@ func TestMovingFundsAction_GetWalletMembersInfo(t *testing.T) { } } +func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { + var tests = map[string]struct { + sourceWalletPublicKeyHash [20]byte + targetWalletsCommitmentHash [32]byte + mainUtxo bitcoin.UnspentTransactionOutput + currentBlock uint64 + walletMemberIDs []uint32 + walletMemberIndex uint32 + targetWallets [][20]byte + expectedError error + }{ + "submission successful": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + // Simulate the commitment has updated. + targetWalletsCommitmentHash: hexToByte32( + "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + ), + mainUtxo: bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: hexToByte32( + "102414558e061ea6e73d5a7bdbf1159b1518c071c22005475d0215ec78a0b911", + ), + OutputIndex: 11, + }, + Value: 111, + }, + walletMemberIDs: []uint32{11, 22, 33, 44}, + walletMemberIndex: 1, + targetWallets: [][20]byte{ + hexToByte20("92a6ec889a8fa34f731e639edede4c75e184307c"), + hexToByte20("fdfa28e238734271f5e0d4f53d3843ae6cc09b24"), + }, + currentBlock: 200000, + expectedError: nil, + }, + "submission unsuccessful": { + sourceWalletPublicKeyHash: hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ), + // Simulate the commitment has not been updated by setting target + // wallets commitment has to zero. The rest of the parameters is + // not important. + targetWalletsCommitmentHash: [32]byte{}, + mainUtxo: bitcoin.UnspentTransactionOutput{}, + walletMemberIDs: []uint32{}, + walletMemberIndex: 0, + targetWallets: [][20]byte{}, + currentBlock: 200000, + expectedError: tbtcpg.ErrTransactionNotIncluded, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + + tbtcChain.SetWallet( + test.sourceWalletPublicKeyHash, + &tbtc.WalletChainData{ + MovingFundsTargetWalletsCommitmentHash: test.targetWalletsCommitmentHash, + }, + ) + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(test.currentBlock) + tbtcChain.SetBlockCounter(blockCounter) + + task := tbtcpg.NewMovingFundsTask(tbtcChain, nil) + + err := task.SubmitMovingFundsCommitment( + test.sourceWalletPublicKeyHash, + &test.mainUtxo, + test.walletMemberIDs, + test.walletMemberIndex, + test.targetWallets, + ) + + testutils.AssertAnyErrorInChainMatchesTarget( + t, + test.expectedError, + err, + ) + + submittedMovingFundsCommitments := tbtcChain.GetMovingFundsSubmissions() + testutils.AssertIntsEqual( + t, + "commitment submission count", + 1, + len(submittedMovingFundsCommitments), + ) + + submittedMovingFundsCommitment := submittedMovingFundsCommitments[0] + + expectedWalletPublicKeyHash := test.sourceWalletPublicKeyHash + actualWalletPublicKeyHash := submittedMovingFundsCommitment.WalletPublicKeyHash + testutils.AssertBytesEqual( + t, + expectedWalletPublicKeyHash[:], + actualWalletPublicKeyHash[:], + ) + + expectedWalletMainUtxo := &test.mainUtxo + actualWalletMainUtxo := submittedMovingFundsCommitment.WalletMainUtxo + if diff := deep.Equal(expectedWalletMainUtxo, actualWalletMainUtxo); diff != nil { + t.Errorf("invalid wallet main utxo: %v", diff) + } + + expectedWalletMemberIDs := test.walletMemberIDs + actualWalletMemberIDs := submittedMovingFundsCommitment.WalletMembersIDs + if diff := deep.Equal(expectedWalletMemberIDs, actualWalletMemberIDs); diff != nil { + t.Errorf("invalid wallet member IDs: %v", diff) + } + + expectedWalletMemberIndex := test.walletMemberIndex + actualWalletMemberIndex := submittedMovingFundsCommitment.WalletMemberIndex + if diff := deep.Equal(expectedWalletMemberIndex, actualWalletMemberIndex); diff != nil { + t.Errorf("invalid wallet member index: %v", diff) + } + + expectedTargetWallets := test.targetWallets + actualTargetWallets := submittedMovingFundsCommitment.TargetWallets + if diff := deep.Equal(expectedTargetWallets, actualTargetWallets); diff != nil { + t.Errorf("invalid target wallets: %v", diff) + } + }) + } +} + type walletInfo struct { publicKeyHash [20]byte state tbtc.WalletState From d847cd259a2869fae7f92185f4fb74ee8ffde42f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 23 Jan 2024 19:18:18 +0100 Subject: [PATCH 28/50] Updated chain interface --- pkg/tbtc/chain.go | 9 --------- pkg/tbtc/chain_test.go | 10 ---------- pkg/tbtcpg/chain.go | 9 +++++++++ 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index d1bb6a8750..dfb73efcca 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -221,15 +221,6 @@ type BridgeChain interface { fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, ) (*DepositChainRequest, bool, error) - - // Submits the moving funds target wallets commitment. - SubmitMovingFundsCommitment( - walletPublicKeyHash [20]byte, - walletMainUTXO bitcoin.UnspentTransactionOutput, - walletMembersIDs []uint32, - walletMemberIndex uint32, - targetWallets [][20]byte, - ) error } // NewWalletRegisteredEvent represents a new wallet registered event. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index de977b8c5c..32857b5d23 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -810,16 +810,6 @@ func buildDepositSweepProposalValidationKey( return sha256.Sum256(buffer.Bytes()), nil } -func (lc *localChain) SubmitMovingFundsCommitment( - walletPublicKeyHash [20]byte, - walletMainUTXO bitcoin.UnspentTransactionOutput, - walletMembersIDs []uint32, - walletMemberIndex uint32, - targetWallets [][20]byte, -) error { - panic("unsupported") -} - func (lc *localChain) ValidateRedemptionProposal( walletPublicKeyHash [20]byte, proposal *RedemptionProposal, diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 58bcd10687..75dda4ec83 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -165,4 +165,13 @@ type Chain interface { mainUTXO *bitcoin.UnspentTransactionOutput, proposal *tbtc.MovingFundsProposal, ) error + + // Submits the moving funds target wallets commitment. + SubmitMovingFundsCommitment( + walletPublicKeyHash [20]byte, + walletMainUTXO bitcoin.UnspentTransactionOutput, + walletMembersIDs []uint32, + walletMemberIndex uint32, + targetWallets [][20]byte, + ) error } From c071c322792ff46d80020b383451921cb280a2f4 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 24 Jan 2024 15:32:44 +0100 Subject: [PATCH 29/50] Added unit tests for estimating transaction fee --- pkg/tbtcpg/moving_funds.go | 6 ++++- pkg/tbtcpg/moving_funds_test.go | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 1d9695632c..5a69bac116 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -52,6 +52,10 @@ var ( ErrTransactionNotIncluded = fmt.Errorf( "transaction not included in blockchain", ) + + // ErrFeeTooHigh is the error returned when the estimated fee exceeds the + // maximum fee allowed for the moving funds transaction. + ErrFeeTooHigh = fmt.Errorf("estimated fee exceeds the maximum fee") ) // MovingFundsCommitmentLookBackPeriod is the look-back period used when @@ -620,7 +624,7 @@ func EstimateMovingFundsFee( } if uint64(totalFee) > txMaxTotalFee { - return 0, fmt.Errorf("estimated fee exceeds the maximum fee") + return 0, ErrFeeTooHigh } return totalFee, nil diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index f2fbe02899..3fc1a16577 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -586,6 +586,53 @@ func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { } } +func TestEstimateMovingFundsFee(t *testing.T) { + var tests = map[string]struct { + txMaxTotalFee uint64 + expectedFee uint64 + expectedError error + }{ + "estimated fee correct": { + txMaxTotalFee: 6000, + expectedFee: 3248, + expectedError: nil, + }, + "estimated fee too high": { + txMaxTotalFee: 3000, + expectedFee: 0, + expectedError: tbtcpg.ErrFeeTooHigh, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + btcChain := tbtcpg.NewLocalBitcoinChain() + btcChain.SetEstimateSatPerVByteFee(1, 16) + + targetWalletsCount := 4 + + actualFee, err := tbtcpg.EstimateMovingFundsFee( + btcChain, + targetWalletsCount, + test.txMaxTotalFee, + ) + + testutils.AssertUintsEqual( + t, + "fee", + test.expectedFee, + uint64(actualFee), + ) + + testutils.AssertAnyErrorInChainMatchesTarget( + t, + test.expectedError, + err, + ) + }) + } +} + type walletInfo struct { publicKeyHash [20]byte state tbtc.WalletState From ddbde936ac45a06824aa2908b7a5b6227523f2ee Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 24 Jan 2024 20:13:54 +0100 Subject: [PATCH 30/50] Added unit tests for moving funds proposing --- pkg/tbtc/moving_funds.go | 9 +- pkg/tbtcpg/chain_test.go | 140 +++++++++++++++++++++++++++++-- pkg/tbtcpg/deposit_sweep_test.go | 2 +- pkg/tbtcpg/moving_funds.go | 5 +- pkg/tbtcpg/moving_funds_test.go | 95 +++++++++++++++++++++ pkg/tbtcpg/redemptions_test.go | 7 +- 6 files changed, 239 insertions(+), 19 deletions(-) diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 6bf294a401..6b210b6547 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -30,10 +30,6 @@ func (mfp *MovingFundsProposal) ValidityBlocks() uint64 { return movingFundsProposalValidityBlocks } -type MovingFundsRequest struct { - TargetWalletPublicKeyHash [20]byte -} - // movingFundsAction is a walletAction implementation handling moving funds // requests from the wallet coordinator. type movingFundsAction struct { @@ -104,7 +100,7 @@ func ValidateMovingFundsProposal( proposal *MovingFundsProposal, ) error }, -) ([]*MovingFundsRequest, error) { +) ([][20]byte, error) { validateProposalLogger.Infof("calling chain for proposal validation") err := chain.ValidateMovingFundsProposal( @@ -119,8 +115,7 @@ func ValidateMovingFundsProposal( ) } - // TODO: Implement - return nil, nil + return proposal.TargetWallets, nil } func (mfa *movingFundsAction) wallet() wallet { diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 534dedd3b4..35589cda99 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -43,6 +43,20 @@ type walletParameters = struct { closingPeriod uint32 } +type movingFundsParameters = struct { + txMaxTotalFee uint64 + dustThreshold uint64 + timeoutResetDelay uint32 + timeout uint32 + timeoutSlashingAmount *big.Int + timeoutNotifierRewardMultiplier uint32 + commitmentGasOffset uint16 + sweepTxMaxTotalFee uint64 + sweepTimeout uint32 + sweepTimeoutSlashingAmount *big.Int + sweepTimeoutNotifierRewardMultiplier uint32 +} + type movingFundsCommitmentSubmission struct { WalletPublicKeyHash [20]byte WalletMainUtxo *bitcoin.UnspentTransactionOutput @@ -68,10 +82,12 @@ type LocalChain struct { averageBlockTime time.Duration pendingRedemptionRequests map[[32]byte]*tbtc.RedemptionRequest redemptionProposalValidations map[[32]byte]bool - pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent heartbeatProposalValidations map[[16]byte]bool - operatorIDs map[chain.Address]uint32 + movingFundsParameters movingFundsParameters + pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent + movingFundsProposalValidations map[[32]byte]bool movingFundsCommitmentSubmissions []*movingFundsCommitmentSubmission + operatorIDs map[chain.Address]uint32 } func NewLocalChain() *LocalChain { @@ -84,10 +100,11 @@ func NewLocalChain() *LocalChain { walletChainData: make(map[[20]byte]*tbtc.WalletChainData), pendingRedemptionRequests: make(map[[32]byte]*tbtc.RedemptionRequest), redemptionProposalValidations: make(map[[32]byte]bool), - pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent), heartbeatProposalValidations: make(map[[16]byte]bool), - operatorIDs: make(map[chain.Address]uint32), + pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*tbtc.MovingFundsCommitmentSubmittedEvent), + movingFundsProposalValidations: make(map[[32]byte]bool), movingFundsCommitmentSubmissions: make([]*movingFundsCommitmentSubmission, 0), + operatorIDs: make(map[chain.Address]uint32), } } @@ -683,7 +700,74 @@ func (lc *LocalChain) GetMovingFundsParameters() ( sweepTimeoutNotifierRewardMultiplier uint32, err error, ) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.movingFundsParameters.txMaxTotalFee, + lc.movingFundsParameters.dustThreshold, + lc.movingFundsParameters.timeoutResetDelay, + lc.movingFundsParameters.timeout, + lc.movingFundsParameters.timeoutSlashingAmount, + lc.movingFundsParameters.timeoutNotifierRewardMultiplier, + lc.movingFundsParameters.commitmentGasOffset, + lc.movingFundsParameters.sweepTxMaxTotalFee, + lc.movingFundsParameters.sweepTimeout, + lc.movingFundsParameters.sweepTimeoutSlashingAmount, + lc.movingFundsParameters.sweepTimeoutNotifierRewardMultiplier, + nil +} + +func (lc *LocalChain) SetMovingFundsParameters( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.movingFundsParameters = movingFundsParameters{ + txMaxTotalFee: txMaxTotalFee, + dustThreshold: dustThreshold, + timeoutResetDelay: timeoutResetDelay, + timeout: timeout, + timeoutSlashingAmount: timeoutSlashingAmount, + timeoutNotifierRewardMultiplier: timeoutNotifierRewardMultiplier, + commitmentGasOffset: commitmentGasOffset, + sweepTxMaxTotalFee: sweepTxMaxTotalFee, + sweepTimeout: sweepTimeout, + sweepTimeoutSlashingAmount: sweepTimeoutSlashingAmount, + sweepTimeoutNotifierRewardMultiplier: sweepTimeoutNotifierRewardMultiplier, + } +} + +func buildMovingFundsProposalValidationKey( + walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, + proposal *tbtc.MovingFundsProposal, +) ([32]byte, error) { + var buffer bytes.Buffer + + buffer.Write(walletPublicKeyHash[:]) + + buffer.Write(mainUTXO.Outpoint.TransactionHash[:]) + binary.Write(&buffer, binary.BigEndian, mainUTXO.Outpoint.OutputIndex) + binary.Write(&buffer, binary.BigEndian, mainUTXO.Value) + + for _, wallet := range proposal.TargetWallets { + buffer.Write(wallet[:]) + } + + buffer.Write(proposal.MovingFundsTxFee.Bytes()) + + return sha256.Sum256(buffer.Bytes()), nil } func (lc *LocalChain) ValidateMovingFundsProposal( @@ -691,7 +775,51 @@ func (lc *LocalChain) ValidateMovingFundsProposal( mainUTXO *bitcoin.UnspentTransactionOutput, proposal *tbtc.MovingFundsProposal, ) error { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key, err := buildMovingFundsProposalValidationKey( + walletPublicKeyHash, + mainUTXO, + proposal, + ) + if err != nil { + return err + } + + result, ok := lc.movingFundsProposalValidations[key] + if !ok { + return fmt.Errorf("validation result unknown") + } + + if !result { + return fmt.Errorf("validation failed") + } + + return nil +} + +func (lc *LocalChain) SetMovingFundsProposalValidationResult( + walletPublicKeyHash [20]byte, + mainUTXO *bitcoin.UnspentTransactionOutput, + proposal *tbtc.MovingFundsProposal, + result bool, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key, err := buildMovingFundsProposalValidationKey( + walletPublicKeyHash, + mainUTXO, + proposal, + ) + if err != nil { + return err + } + + lc.movingFundsProposalValidations[key] = result + + return nil } func buildRedemptionProposalValidationKey( diff --git a/pkg/tbtcpg/deposit_sweep_test.go b/pkg/tbtcpg/deposit_sweep_test.go index 7cbbe6d9a7..add13aed70 100644 --- a/pkg/tbtcpg/deposit_sweep_test.go +++ b/pkg/tbtcpg/deposit_sweep_test.go @@ -176,7 +176,7 @@ func TestDepositSweepTask_ProposeDepositsSweep(t *testing.T) { actualDepositSweepProposals, expectedDepositSweepProposals, ); diff != nil { - t.Errorf("invalid deposits: %v", diff) + t.Errorf("invalid deposit sweep proposal: %v", diff) } }) } diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 5a69bac116..5e791a913d 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -545,9 +545,10 @@ func (mft *MovingFundsTask) ProposeMovingFunds( taskLogger.Infof("preparing a moving funds proposal") + // Estimate fee if it's missing. if fee <= 0 { taskLogger.Infof("estimating moving funds transaction fee") - var err error + txMaxTotalFee, _, _, _, _, _, _, _, _, _, _, err := mft.chain.GetMovingFundsParameters() if err != nil { return nil, fmt.Errorf( @@ -585,7 +586,7 @@ func (mft *MovingFundsTask) ProposeMovingFunds( mft.chain, ); err != nil { return nil, fmt.Errorf( - "failed to verify moving funds proposal: %w", + "failed to verify moving funds proposal: [%w]", err, ) } diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index 3fc1a16577..fe13eee629 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -2,6 +2,7 @@ package tbtcpg_test import ( "encoding/hex" + "math/big" "reflect" "testing" "time" @@ -586,6 +587,100 @@ func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { } } +func TestMovingFundsAction_ProposeMovingFunds(t *testing.T) { + walletPublicKeyHash := hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ) + + targetWallets := [][20]byte{ + hexToByte20("92a6ec889a8fa34f731e639edede4c75e184307c"), + hexToByte20("fdfa28e238734271f5e0d4f53d3843ae6cc09b24"), + hexToByte20("c7302d75072d78be94eb8d36c4b77583c7abb06e"), + } + + mainUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: hexToByte32( + "102414558e061ea6e73d5a7bdbf1159b1518c071c22005475d0215ec78a0b911", + ), + OutputIndex: 11, + }, + Value: 111, + } + + txMaxTotalFee := uint64(6000) + + var tests = map[string]struct { + fee int64 + expectedProposal *tbtc.MovingFundsProposal + }{ + "fee provided": { + fee: 10000, + expectedProposal: &tbtc.MovingFundsProposal{ + TargetWallets: targetWallets, + MovingFundsTxFee: big.NewInt(10000), + }, + }, + "fee estimated": { + fee: 0, // trigger fee estimation + expectedProposal: &tbtc.MovingFundsProposal{ + TargetWallets: targetWallets, + MovingFundsTxFee: big.NewInt(4300), + }, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + btcChain.SetEstimateSatPerVByteFee(1, 25) + + tbtcChain.SetMovingFundsParameters( + txMaxTotalFee, + 0, + 0, + 0, + nil, + 0, + 0, + 0, + 0, + nil, + 0, + ) + + err := tbtcChain.SetMovingFundsProposalValidationResult( + walletPublicKeyHash, + mainUtxo, + test.expectedProposal, + true, + ) + if err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewMovingFundsTask(tbtcChain, btcChain) + + proposal, err := task.ProposeMovingFunds( + &testutils.MockLogger{}, + walletPublicKeyHash, + mainUtxo, + targetWallets, + test.fee, + ) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal(proposal, test.expectedProposal); diff != nil { + t.Errorf("invalid moving funds proposal: %v", diff) + } + }) + } +} + func TestEstimateMovingFundsFee(t *testing.T) { var tests = map[string]struct { txMaxTotalFee uint64 diff --git a/pkg/tbtcpg/redemptions_test.go b/pkg/tbtcpg/redemptions_test.go index 205f5f7c75..c9dff57417 100644 --- a/pkg/tbtcpg/redemptions_test.go +++ b/pkg/tbtcpg/redemptions_test.go @@ -2,14 +2,15 @@ package tbtcpg_test import ( "encoding/hex" + "math/big" + "testing" + "github.com/go-test/deep" "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/tbtcpg" "github.com/keep-network/keep-core/pkg/tbtcpg/internal/test" - "math/big" - "testing" ) // Test based on example testnet redemption transaction: @@ -203,7 +204,7 @@ func TestRedemptionAction_ProposeRedemption(t *testing.T) { } if diff := deep.Equal(proposal, test.expectedProposal); diff != nil { - t.Errorf("invalid deposits: %v", diff) + t.Errorf("invalid redemption proposal: %v", diff) } }) } From 38c2ab27336c78319b673b5ed6753084dcffefcc Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 25 Jan 2024 12:50:07 +0100 Subject: [PATCH 31/50] Minor refactor of unit tests --- pkg/tbtcpg/moving_funds_test.go | 156 +++++++++++++------------------- 1 file changed, 63 insertions(+), 93 deletions(-) diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index fe13eee629..034188ac57 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -3,7 +3,6 @@ package tbtcpg_test import ( "encoding/hex" "math/big" - "reflect" "testing" "time" @@ -17,19 +16,19 @@ import ( ) func TestMovingFundsAction_FindTargetWallets_CommitmentNotSubmittedYet(t *testing.T) { + walletPublicKeyHash := hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ) + var tests = map[string]struct { - sourceWalletPublicKeyHash [20]byte - walletBalance uint64 - walletMaxBtcTransfer uint64 - registeredWallets []walletInfo - liveWalletsCount uint32 - expectedTargetWallets [][20]byte - expectedError error + walletBalance uint64 + walletMaxBtcTransfer uint64 + registeredWallets []walletInfo + liveWalletsCount uint32 + expectedTargetWallets [][20]byte + expectedError error }{ "success scenario": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), walletBalance: 2000000, walletMaxBtcTransfer: 300000, registeredWallets: []walletInfo{ @@ -85,9 +84,6 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentNotSubmittedYet(t *testin expectedError: nil, }, "wallet max BTC transfer is zero": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), walletBalance: 10000, walletMaxBtcTransfer: 0, // Set to zero. registeredWallets: []walletInfo{}, @@ -96,9 +92,6 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentNotSubmittedYet(t *testin expectedError: tbtcpg.ErrMaxBtcTransferZero, }, "not enough live wallets": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), walletBalance: 2000000, walletMaxBtcTransfer: 300000, // Simulate there should be two Live wallets, but set only one Live @@ -165,18 +158,17 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentNotSubmittedYet(t *testin targetWallets, alreadySubmitted, err := task.FindTargetWallets( &testutils.MockLogger{}, - test.sourceWalletPublicKeyHash, + walletPublicKeyHash, targetWalletsCommitmentHash, test.walletBalance, test.liveWalletsCount, ) - if !reflect.DeepEqual(test.expectedTargetWallets, targetWallets) { - t.Errorf( - "unexpected target wallets\nexpected: %v\nactual: %v", - test.expectedTargetWallets, - targetWallets, - ) + if diff := deep.Equal( + test.expectedTargetWallets, + targetWallets, + ); diff != nil { + t.Errorf("unexpected target wallets: %v", diff) } // Returned value for the already submitted commitment should @@ -199,19 +191,21 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentNotSubmittedYet(t *testin } func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testing.T) { + walletPublicKeyHash := hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ) + + currentBlock := uint64(1000000) + + averageBlockTime := 10 * time.Second + var tests = map[string]struct { - sourceWalletPublicKeyHash [20]byte targetWalletsCommitmentHash [32]byte targetWallets []walletInfo - currentBlock uint64 - averageBlockTime time.Duration expectedTargetWallets [][20]byte expectedError error }{ "success scenario": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), targetWalletsCommitmentHash: hexToByte32( "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", ), @@ -235,8 +229,6 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi state: tbtc.StateLive, }, }, - currentBlock: 1000000, - averageBlockTime: 10 * time.Second, expectedTargetWallets: [][20]byte{ hexToByte20("92a6ec889a8fa34f731e639edede4c75e184307c"), hexToByte20("c7302d75072d78be94eb8d36c4b77583c7abb06e"), @@ -245,9 +237,6 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi expectedError: nil, }, "target wallet is not live": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), targetWalletsCommitmentHash: hexToByte32( "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", ), @@ -271,15 +260,10 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi state: tbtc.StateLive, }, }, - currentBlock: 1000000, - averageBlockTime: 10 * time.Second, expectedTargetWallets: nil, expectedError: tbtcpg.ErrTargetWalletNotLive, }, "target wallet commitment hash mismatch": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), targetWalletsCommitmentHash: hexToByte32( "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", ), @@ -291,8 +275,6 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi state: tbtc.StateLive, }, }, - currentBlock: 1000000, - averageBlockTime: 10 * time.Second, expectedTargetWallets: nil, expectedError: tbtcpg.ErrWrongCommitmentHash, }, @@ -302,15 +284,15 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi t.Run(testName, func(t *testing.T) { tbtcChain := tbtcpg.NewLocalChain() - tbtcChain.SetAverageBlockTime(test.averageBlockTime) + tbtcChain.SetAverageBlockTime(averageBlockTime) blockCounter := tbtcpg.NewMockBlockCounter() - blockCounter.SetCurrentBlock(test.currentBlock) + blockCounter.SetCurrentBlock(currentBlock) tbtcChain.SetBlockCounter(blockCounter) - startBlock := test.currentBlock - uint64( + startBlock := currentBlock - uint64( tbtcpg.MovingFundsCommitmentLookBackPeriod.Seconds(), - )/uint64(test.averageBlockTime.Seconds()) + )/uint64(averageBlockTime.Seconds()) targetWallets := [][20]byte{} for _, walletInfo := range test.targetWallets { @@ -323,10 +305,8 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi err := tbtcChain.AddPastMovingFundsCommitmentSubmittedEvent( &tbtc.MovingFundsCommitmentSubmittedEventFilter{ - StartBlock: startBlock, - WalletPublicKeyHash: [][20]byte{ - test.sourceWalletPublicKeyHash, - }, + StartBlock: startBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, }, &tbtc.MovingFundsCommitmentSubmittedEvent{ TargetWallets: targetWallets, @@ -345,18 +325,17 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi targetWallets, alreadySubmitted, err := task.FindTargetWallets( &testutils.MockLogger{}, - test.sourceWalletPublicKeyHash, + walletPublicKeyHash, test.targetWalletsCommitmentHash, walletBalance, liveWalletsCount, ) - if !reflect.DeepEqual(test.expectedTargetWallets, targetWallets) { - t.Errorf( - "unexpected target wallets\nexpected: %v\nactual: %v", - test.expectedTargetWallets, - targetWallets, - ) + if diff := deep.Equal( + test.expectedTargetWallets, + targetWallets, + ); diff != nil { + t.Errorf("unexpected target wallets: %v", diff) } // Returned value for the already submitted commitment should @@ -433,12 +412,8 @@ func TestMovingFundsAction_GetWalletMembersInfo(t *testing.T) { test.executingOperator, ) - if !reflect.DeepEqual(test.expectedMemberIDs, memberIDs) { - t.Errorf( - "unexpected memberIDs\nexpected: %v\nactual: %v", - test.expectedMemberIDs, - memberIDs, - ) + if diff := deep.Equal(test.expectedMemberIDs, memberIDs); diff != nil { + t.Errorf("unexpected memberIDs: %v", diff) } testutils.AssertUintsEqual( @@ -458,55 +433,50 @@ func TestMovingFundsAction_GetWalletMembersInfo(t *testing.T) { } func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { + walletPublicKeyHash := hexToByte20( + "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", + ) + + walletMainUtxo := bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: hexToByte32( + "102414558e061ea6e73d5a7bdbf1159b1518c071c22005475d0215ec78a0b911", + ), + OutputIndex: 11, + }, + Value: 111, + } + + currentBlock := uint64(200000) + var tests = map[string]struct { - sourceWalletPublicKeyHash [20]byte targetWalletsCommitmentHash [32]byte - mainUtxo bitcoin.UnspentTransactionOutput - currentBlock uint64 walletMemberIDs []uint32 walletMemberIndex uint32 targetWallets [][20]byte expectedError error }{ "submission successful": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), // Simulate the commitment has updated. targetWalletsCommitmentHash: hexToByte32( "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", ), - mainUtxo: bitcoin.UnspentTransactionOutput{ - Outpoint: &bitcoin.TransactionOutpoint{ - TransactionHash: hexToByte32( - "102414558e061ea6e73d5a7bdbf1159b1518c071c22005475d0215ec78a0b911", - ), - OutputIndex: 11, - }, - Value: 111, - }, walletMemberIDs: []uint32{11, 22, 33, 44}, walletMemberIndex: 1, targetWallets: [][20]byte{ hexToByte20("92a6ec889a8fa34f731e639edede4c75e184307c"), hexToByte20("fdfa28e238734271f5e0d4f53d3843ae6cc09b24"), }, - currentBlock: 200000, expectedError: nil, }, "submission unsuccessful": { - sourceWalletPublicKeyHash: hexToByte20( - "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", - ), // Simulate the commitment has not been updated by setting target // wallets commitment has to zero. The rest of the parameters is // not important. targetWalletsCommitmentHash: [32]byte{}, - mainUtxo: bitcoin.UnspentTransactionOutput{}, walletMemberIDs: []uint32{}, walletMemberIndex: 0, targetWallets: [][20]byte{}, - currentBlock: 200000, expectedError: tbtcpg.ErrTransactionNotIncluded, }, } @@ -516,21 +486,21 @@ func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { tbtcChain := tbtcpg.NewLocalChain() tbtcChain.SetWallet( - test.sourceWalletPublicKeyHash, + walletPublicKeyHash, &tbtc.WalletChainData{ MovingFundsTargetWalletsCommitmentHash: test.targetWalletsCommitmentHash, }, ) blockCounter := tbtcpg.NewMockBlockCounter() - blockCounter.SetCurrentBlock(test.currentBlock) + blockCounter.SetCurrentBlock(currentBlock) tbtcChain.SetBlockCounter(blockCounter) task := tbtcpg.NewMovingFundsTask(tbtcChain, nil) err := task.SubmitMovingFundsCommitment( - test.sourceWalletPublicKeyHash, - &test.mainUtxo, + walletPublicKeyHash, + &walletMainUtxo, test.walletMemberIDs, test.walletMemberIndex, test.targetWallets, @@ -552,7 +522,7 @@ func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { submittedMovingFundsCommitment := submittedMovingFundsCommitments[0] - expectedWalletPublicKeyHash := test.sourceWalletPublicKeyHash + expectedWalletPublicKeyHash := walletPublicKeyHash actualWalletPublicKeyHash := submittedMovingFundsCommitment.WalletPublicKeyHash testutils.AssertBytesEqual( t, @@ -560,7 +530,7 @@ func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { actualWalletPublicKeyHash[:], ) - expectedWalletMainUtxo := &test.mainUtxo + expectedWalletMainUtxo := &walletMainUtxo actualWalletMainUtxo := submittedMovingFundsCommitment.WalletMainUtxo if diff := deep.Equal(expectedWalletMainUtxo, actualWalletMainUtxo); diff != nil { t.Errorf("invalid wallet main utxo: %v", diff) @@ -598,7 +568,7 @@ func TestMovingFundsAction_ProposeMovingFunds(t *testing.T) { hexToByte20("c7302d75072d78be94eb8d36c4b77583c7abb06e"), } - mainUtxo := &bitcoin.UnspentTransactionOutput{ + walletMainUtxo := &bitcoin.UnspentTransactionOutput{ Outpoint: &bitcoin.TransactionOutpoint{ TransactionHash: hexToByte32( "102414558e061ea6e73d5a7bdbf1159b1518c071c22005475d0215ec78a0b911", @@ -653,7 +623,7 @@ func TestMovingFundsAction_ProposeMovingFunds(t *testing.T) { err := tbtcChain.SetMovingFundsProposalValidationResult( walletPublicKeyHash, - mainUtxo, + walletMainUtxo, test.expectedProposal, true, ) @@ -666,7 +636,7 @@ func TestMovingFundsAction_ProposeMovingFunds(t *testing.T) { proposal, err := task.ProposeMovingFunds( &testutils.MockLogger{}, walletPublicKeyHash, - mainUtxo, + walletMainUtxo, targetWallets, test.fee, ) From d7086753d5ad52da69ada7e0e3a5fdd5164e4617 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 26 Jan 2024 16:02:34 +0100 Subject: [PATCH 32/50] Minor corrections --- pkg/tbtcpg/moving_funds.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 5e791a913d..9beb9d9fc8 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -25,9 +25,7 @@ var ( // ErrNotEnoughTargetWallets is the error returned when the number of // gathered target wallets does not match the required target wallets count. - ErrNotEnoughTargetWallets = fmt.Errorf( - "not enough target wallets", - ) + ErrNotEnoughTargetWallets = fmt.Errorf("not enough target wallets") // ErrWrongCommitmentHash is the error returned when the hash calculated // from retrieved target wallets does not match the committed hash. @@ -37,9 +35,7 @@ var ( // ErrTargetWalletNotLive is the error returned when a target wallet is not // in the Live state. - ErrTargetWalletNotLive = fmt.Errorf( - "target wallet is not live", - ) + ErrTargetWalletNotLive = fmt.Errorf("target wallet is not live") // ErrNoExecutingOperator is the error returned when the task executing // operator is not found among the wallet operator IDs. @@ -121,7 +117,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( ) if err != nil { return nil, false, fmt.Errorf( - "cannot get wallet's maun UTXO: [%w]", + "cannot get wallet's main UTXO: [%w]", err, ) } From 1d5a72332abf957d01fa83bd86b33d01e19d7d2d Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 2 Feb 2024 17:01:57 +0100 Subject: [PATCH 33/50] Added additional logs --- pkg/tbtc/moving_funds.go | 2 ++ pkg/tbtcpg/moving_funds.go | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 6b210b6547..96c7cf9128 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -115,6 +115,8 @@ func ValidateMovingFundsProposal( ) } + validateProposalLogger.Infof("moving funds proposal is valid") + return proposal.TargetWallets, nil } diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 9beb9d9fc8..c043ae29d7 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -277,7 +277,7 @@ func (mft *MovingFundsTask) findNewTargetWallets( // 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. + // are located there and have the highest chance of being Live. events, err := mft.chain.PastNewWalletRegisteredEvents(nil) if err != nil { return nil, fmt.Errorf( @@ -568,12 +568,15 @@ func (mft *MovingFundsTask) ProposeMovingFunds( fee = estimatedFee } + taskLogger.Infof("moving funds transaction fee: [%d]", fee) + proposal := &tbtc.MovingFundsProposal{ TargetWallets: targetWallets, MovingFundsTxFee: big.NewInt(fee), } taskLogger.Infof("validating the moving funds proposal") + if _, err := tbtc.ValidateMovingFundsProposal( taskLogger, walletPublicKeyHash, From 6d5755ff6f1bc5847a11b73c3442e106af2d5c35 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 2 Feb 2024 17:23:02 +0100 Subject: [PATCH 34/50] Added check for wallet hash length during unmarshalling --- pkg/tbtc/marshaling.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 9eea456447..ec9aa32277 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -423,6 +423,9 @@ func (mfp *MovingFundsProposal) Unmarshal(data []byte) error { mfp.TargetWallets = make([][20]byte, len(pbMsg.TargetWallets)) for i, wallet := range pbMsg.TargetWallets { + if len(wallet) != 20 { + return fmt.Errorf("invalid target wallet length: [%v]", len(wallet)) + } copy(mfp.TargetWallets[i][:], wallet) } From 09dd59d5528a46ca7b84b620c2ce2a528b0a2b2a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 2 Feb 2024 17:28:58 +0100 Subject: [PATCH 35/50] Renamed message --- pkg/tbtc/marshaling_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/tbtc/marshaling_test.go b/pkg/tbtc/marshaling_test.go index e9fc678e9a..da3ddfe5d7 100644 --- a/pkg/tbtc/marshaling_test.go +++ b/pkg/tbtc/marshaling_test.go @@ -241,14 +241,14 @@ func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithHeartbeatProposal(t *te f.Fuzz(&walletPublicKeyHash) f.Fuzz(&proposal) - doneMessage := &coordinationMessage{ + coordinationMsg := &coordinationMessage{ senderID: senderID, coordinationBlock: coordinationBlock, walletPublicKeyHash: walletPublicKeyHash, proposal: &proposal, } - _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + _ = pbutils.RoundTrip(coordinationMsg, &coordinationMessage{}) } } @@ -270,14 +270,14 @@ func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithDepositSweepProposal(t f.Fuzz(&walletPublicKeyHash) f.Fuzz(&proposal) - doneMessage := &coordinationMessage{ + coordinationMsg := &coordinationMessage{ senderID: senderID, coordinationBlock: coordinationBlock, walletPublicKeyHash: walletPublicKeyHash, proposal: &proposal, } - _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + _ = pbutils.RoundTrip(coordinationMsg, &coordinationMessage{}) } } @@ -299,14 +299,14 @@ func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithRedemptionProposal(t *t f.Fuzz(&walletPublicKeyHash) f.Fuzz(&proposal) - doneMessage := &coordinationMessage{ + coordinationMsg := &coordinationMessage{ senderID: senderID, coordinationBlock: coordinationBlock, walletPublicKeyHash: walletPublicKeyHash, proposal: &proposal, } - _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + _ = pbutils.RoundTrip(coordinationMsg, &coordinationMessage{}) } } @@ -328,14 +328,14 @@ func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithMovingFundsProposal(t * f.Fuzz(&walletPublicKeyHash) f.Fuzz(&proposal) - doneMessage := &coordinationMessage{ + coordinationMsg := &coordinationMessage{ senderID: senderID, coordinationBlock: coordinationBlock, walletPublicKeyHash: walletPublicKeyHash, proposal: &proposal, } - _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + _ = pbutils.RoundTrip(coordinationMsg, &coordinationMessage{}) } } @@ -357,14 +357,14 @@ func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithNoopProposal(t *testing f.Fuzz(&walletPublicKeyHash) f.Fuzz(&proposal) - doneMessage := &coordinationMessage{ + coordinationMsg := &coordinationMessage{ senderID: senderID, coordinationBlock: coordinationBlock, walletPublicKeyHash: walletPublicKeyHash, proposal: &proposal, } - _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + _ = pbutils.RoundTrip(coordinationMsg, &coordinationMessage{}) } } From e53fcd1eba6c9f8464583072c609d1c6219c5023 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 2 Feb 2024 17:36:19 +0100 Subject: [PATCH 36/50] Removed package rename --- pkg/tbtc/node.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index a1086f8f01..295657b236 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -9,7 +9,7 @@ import ( "sync" "github.com/keep-network/keep-core/pkg/bitcoin" - chainpkg "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/chain" "go.uber.org/zap" @@ -146,7 +146,7 @@ func newNode( } // operatorAddress returns the node's operator address. -func (n *node) operatorAddress() (chainpkg.Address, error) { +func (n *node) operatorAddress() (chain.Address, error) { _, operatorPublicKey, err := n.chain.OperatorKeyPair() if err != nil { return "", fmt.Errorf("failed to get operator public key: [%v]", err) @@ -164,7 +164,7 @@ func (n *node) operatorAddress() (chainpkg.Address, error) { } // operatorAddress returns the node's operator ID. -func (n *node) operatorID() (chainpkg.OperatorID, error) { +func (n *node) operatorID() (chain.OperatorID, error) { operatorAddress, err := n.operatorAddress() if err != nil { return 0, fmt.Errorf("failed to get operator address: [%v]", err) From c7bb47eeb9bec9d7b8634c0bbd87a6b89fa6ec23 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 2 Feb 2024 17:38:50 +0100 Subject: [PATCH 37/50] Added function docstring --- pkg/tbtc/node.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 295657b236..4c097b590d 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -608,6 +608,8 @@ func (n *node) handleRedemptionProposal( walletActionLogger.Infof("wallet action dispatched successfully") } +// handleMovingFundsProposal handles an incoming moving funds proposal by +// orchestrating and dispatching an appropriate wallet action. func (n *node) handleMovingFundsProposal( wallet wallet, proposal *MovingFundsProposal, From 5a4e2f2f53cda586f4afebbebde8f44bfd2fc1fe Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 2 Feb 2024 17:40:43 +0100 Subject: [PATCH 38/50] Changed return value of function --- pkg/tbtcpg/moving_funds.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index c043ae29d7..ef60e927b9 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -202,7 +202,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( } } - return proposal, false, nil + return proposal, true, nil } // FindTargetWallets returns a list of target wallets for the moving funds From a013fa6dec37e4ce122f68386d8e0f5d920e99c5 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 5 Feb 2024 12:01:01 +0100 Subject: [PATCH 39/50] Simplified commitment look back blocks calculation --- pkg/tbtcpg/moving_funds.go | 19 ++++++++++++------- pkg/tbtcpg/moving_funds_test.go | 4 +--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index ef60e927b9..4c8933f473 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -5,7 +5,6 @@ import ( "fmt" "math/big" "sort" - "time" "github.com/ethereum/go-ethereum/crypto" "github.com/ipfs/go-log/v2" @@ -54,9 +53,10 @@ var ( ErrFeeTooHigh = fmt.Errorf("estimated fee exceeds the maximum fee") ) -// MovingFundsCommitmentLookBackPeriod is the look-back period used when -// searching for submitted moving funds commitment events. -const MovingFundsCommitmentLookBackPeriod = 30 * 24 * time.Hour // 1 month +// MovingFundsCommitmentLookBackBlocks is the look-back period in blocks used +// when searching for submitted moving funds commitment events. It's equal to +// 30 days assuming 12 seconds per block. +const MovingFundsCommitmentLookBackBlocks = uint64(216000) // MovingFundsTask is a task that may produce a moving funds proposal. type MovingFundsTask struct { @@ -362,9 +362,14 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( ) } - filterLookBackSeconds := uint64(MovingFundsCommitmentLookBackPeriod.Seconds()) - filterLookBackBlocks := filterLookBackSeconds / uint64(mft.chain.AverageBlockTime().Seconds()) - filterStartBlock := currentBlockNumber - filterLookBackBlocks + // When calculating the filter start block make sure the current block is + // greater than the commitment look back blocks. This condition could be + // unmet for example when running local tests. In that case keep the filter + // start block at `0`. + filterStartBlock := uint64(0) + if currentBlockNumber > MovingFundsCommitmentLookBackBlocks { + filterStartBlock = currentBlockNumber - MovingFundsCommitmentLookBackBlocks + } filter := &tbtc.MovingFundsCommitmentSubmittedEventFilter{ StartBlock: filterStartBlock, diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index 034188ac57..2178d5b4bd 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -290,9 +290,7 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi blockCounter.SetCurrentBlock(currentBlock) tbtcChain.SetBlockCounter(blockCounter) - startBlock := currentBlock - uint64( - tbtcpg.MovingFundsCommitmentLookBackPeriod.Seconds(), - )/uint64(averageBlockTime.Seconds()) + startBlock := currentBlock - tbtcpg.MovingFundsCommitmentLookBackBlocks targetWallets := [][20]byte{} for _, walletInfo := range test.targetWallets { From d2b5cc07312880c2ec8154f9ab670a6676aa4f6c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 5 Feb 2024 13:04:25 +0100 Subject: [PATCH 40/50] Moved commitment hash calculation to the Tbtc chain --- pkg/chain/ethereum/tbtc.go | 20 +++++++++++++++++ pkg/chain/ethereum/tbtc_test.go | 39 +++++++++++++++++++++++++++++++++ pkg/tbtcpg/chain.go | 4 ++++ pkg/tbtcpg/chain_test.go | 15 +++++++++++++ pkg/tbtcpg/moving_funds.go | 12 ++-------- pkg/tbtcpg/moving_funds_test.go | 6 ++--- 6 files changed, 83 insertions(+), 13 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 5292b2504f..b74e81c02f 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "encoding/binary" + "fmt" "math/big" "reflect" @@ -1236,6 +1237,25 @@ func computeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte { return mainUtxoHash } +func (tc *TbtcChain) ComputeMovingFundsCommitmentHash( + targetWallets [][20]byte, +) [32]byte { + return computeMovingFundsCommitmentHash(targetWallets) +} + +func computeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte { + packedWallets := []byte{} + + for _, wallet := range targetWallets { + packedWallets = append(packedWallets, wallet[:]...) + // Each wallet hash must be padded with 12 zero bytes following the + // actual hash. + packedWallets = append(packedWallets, make([]byte, 12)...) + } + + return crypto.Keccak256Hash(packedWallets) +} + func (tc *TbtcChain) BuildDepositKey( fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index 99299bd0a5..294f5a34db 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -338,6 +338,45 @@ func TestComputeMainUtxoHash(t *testing.T) { testutils.AssertBytesEqual(t, expectedMainUtxoHash, mainUtxoHash[:]) } +func TestComputeMovingFundsCommitmentHash(t *testing.T) { + toByte20 := func(s string) [20]byte { + bytes, err := hex.DecodeString(s) + if err != nil { + t.Fatal(err) + } + + if len(bytes) != 20 { + t.Fatal("incorrect hexstring length") + } + + var result [20]byte + copy(result[:], bytes[:]) + return result + } + + targetWallets := [][20]byte{ + toByte20("4b440cb29c80c3f256212d8fdd4f2125366f3c91"), + toByte20("888f01315e0268bfa05d5e522f8d63f6824d9a96"), + toByte20("b2a89e53a4227dbe530a52a1c419040735fa636c"), + } + + movingFundsCommitmentHash := computeMovingFundsCommitmentHash( + targetWallets, + ) + + expectedMovingFundsCommitmentHash, err := hex.DecodeString( + "8ba62d1d754a3429e2ff1fb4f523b5fad2b605c873a2968bb5985a625eb96202", + ) + if err != nil { + t.Fatal(err) + } + testutils.AssertBytesEqual( + t, + expectedMovingFundsCommitmentHash, + movingFundsCommitmentHash[:], + ) +} + // Test data based on: https://etherscan.io/tx/0x97c7a293127a604da77f7ef8daf4b19da2bf04327dd891b6d717eaef89bd8bca func TestBuildDepositKey(t *testing.T) { fundingTxHash, err := bitcoin.NewHashFromString( diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 75dda4ec83..f9887973be 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -174,4 +174,8 @@ type Chain interface { walletMemberIndex uint32, targetWallets [][20]byte, ) error + + // Computes the moving funds commitment hash from the provided public key + // hashes of target wallets. + ComputeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte } diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 35589cda99..f9f21690ea 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -11,6 +11,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" @@ -1000,6 +1002,19 @@ func (lc *LocalChain) ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOu panic("unsupported") } +func (lc *LocalChain) ComputeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte { + packedWallets := []byte{} + + for _, wallet := range targetWallets { + packedWallets = append(packedWallets, wallet[:]...) + // Each wallet hash must be padded with 12 zero bytes following the + // actual hash. + packedWallets = append(packedWallets, make([]byte, 12)...) + } + + return crypto.Keccak256Hash(packedWallets) +} + func (lc *LocalChain) AddPastMovingFundsCommitmentSubmittedEvent( filter *tbtc.MovingFundsCommitmentSubmittedEventFilter, event *tbtc.MovingFundsCommitmentSubmittedEvent, diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 4c8933f473..c8ceb00309 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -1,12 +1,10 @@ package tbtcpg import ( - "bytes" "fmt" "math/big" "sort" - "github.com/ethereum/go-ethereum/crypto" "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-core/pkg/bitcoin" @@ -417,14 +415,8 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( // Just in case check if the hash of the target wallets matches the moving // funds target wallets commitment hash. - packedWallets := []byte{} - for _, wallet := range targetWallets { - packedWallets = append(packedWallets, wallet[:]...) - } - - calculatedHash := crypto.Keccak256(packedWallets) - - if !bytes.Equal(calculatedHash, targetWalletsCommitmentHash[:]) { + calculatedHash := mft.chain.ComputeMovingFundsCommitmentHash(targetWallets) + if calculatedHash != targetWalletsCommitmentHash { return nil, ErrWrongCommitmentHash } diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index 2178d5b4bd..c7464395d1 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -207,7 +207,7 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi }{ "success scenario": { targetWalletsCommitmentHash: hexToByte32( - "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + "7820cd666bf13bda0850e52cfacf64140716e578f7f6a0567cae9b002fc83775", ), targetWallets: []walletInfo{ { @@ -238,7 +238,7 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi }, "target wallet is not live": { targetWalletsCommitmentHash: hexToByte32( - "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + "7820cd666bf13bda0850e52cfacf64140716e578f7f6a0567cae9b002fc83775", ), targetWallets: []walletInfo{ { @@ -265,7 +265,7 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi }, "target wallet commitment hash mismatch": { targetWalletsCommitmentHash: hexToByte32( - "9d9368117956680760fa27bb9542ceba2d4fcc398d640a5a0769f5a9593afb0e", + "7820cd666bf13bda0850e52cfacf64140716e578f7f6a0567cae9b002fc83775", ), targetWallets: []walletInfo{ { // Use only one target wallet. From 7f1613806b15edd347a82adef6211830216aa8e6 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 5 Feb 2024 15:24:26 +0100 Subject: [PATCH 41/50] Renamed variable --- pkg/chain/ethereum/tbtc.go | 1 - pkg/tbtcpg/moving_funds.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index b74e81c02f..a871df7e59 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -4,7 +4,6 @@ import ( "crypto/ecdsa" "crypto/elliptic" "encoding/binary" - "fmt" "math/big" "reflect" diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index c8ceb00309..a9843bffb6 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -148,7 +148,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( targetWalletsCommitmentHash := walletChainData.MovingFundsTargetWalletsCommitmentHash - targetWallets, commitmentSubmitted, err := mft.FindTargetWallets( + targetWallets, commitmentExists, err := mft.FindTargetWallets( taskLogger, walletPublicKeyHash, targetWalletsCommitmentHash, @@ -173,7 +173,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( ) } - if !commitmentSubmitted { + if !commitmentExists { walletMemberIDs, walletMemberIndex, err := mft.GetWalletMembersInfo( request.WalletOperators, request.ExecutingOperator, From c41dc9e0721776be694a7f519e702b57a8cc6fd8 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 5 Feb 2024 15:29:51 +0100 Subject: [PATCH 42/50] Removed unnecessary check --- pkg/tbtcpg/moving_funds.go | 24 ------------------------ pkg/tbtcpg/moving_funds_test.go | 27 --------------------------- 2 files changed, 51 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index a9843bffb6..4851b752a1 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -30,10 +30,6 @@ var ( "target wallets hash must match commitment hash", ) - // ErrTargetWalletNotLive is the error returned when a target wallet is not - // in the Live state. - ErrTargetWalletNotLive = fmt.Errorf("target wallet is not live") - // ErrNoExecutingOperator is the error returned when the task executing // operator is not found among the wallet operator IDs. ErrNoExecutingOperator = fmt.Errorf( @@ -393,26 +389,6 @@ func (mft *MovingFundsTask) retrieveCommittedTargetWallets( targetWallets := events[0].TargetWallets - // Make sure all the target wallets are Live. - for _, targetWallet := range targetWallets { - targetWalletChainData, err := mft.chain.GetWallet(targetWallet) - if err != nil { - return nil, fmt.Errorf( - "failed to get wallet data for target wallet [0x%x]: [%w]", - targetWallet, - err, - ) - } - - if targetWalletChainData.State != tbtc.StateLive { - return nil, fmt.Errorf( - "%w: [0x%x]", - ErrTargetWalletNotLive, - targetWallet, - ) - } - } - // Just in case check if the hash of the target wallets matches the moving // funds target wallets commitment hash. calculatedHash := mft.chain.ComputeMovingFundsCommitmentHash(targetWallets) diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index c7464395d1..f3f3ffac23 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -236,33 +236,6 @@ func TestMovingFundsAction_FindTargetWallets_CommitmentAlreadySubmitted(t *testi }, expectedError: nil, }, - "target wallet is not live": { - targetWalletsCommitmentHash: hexToByte32( - "7820cd666bf13bda0850e52cfacf64140716e578f7f6a0567cae9b002fc83775", - ), - targetWallets: []walletInfo{ - { - publicKeyHash: hexToByte20( - "92a6ec889a8fa34f731e639edede4c75e184307c", - ), - state: tbtc.StateLive, - }, - { - publicKeyHash: hexToByte20( - "c7302d75072d78be94eb8d36c4b77583c7abb06e", - ), - state: tbtc.StateTerminated, // wrong state - }, - { - publicKeyHash: hexToByte20( - "fdfa28e238734271f5e0d4f53d3843ae6cc09b24", - ), - state: tbtc.StateLive, - }, - }, - expectedTargetWallets: nil, - expectedError: tbtcpg.ErrTargetWalletNotLive, - }, "target wallet commitment hash mismatch": { targetWalletsCommitmentHash: hexToByte32( "7820cd666bf13bda0850e52cfacf64140716e578f7f6a0567cae9b002fc83775", From 1d487588f893fd2192b6d9c079f07b1361b918aa Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 6 Feb 2024 15:38:45 +0100 Subject: [PATCH 43/50] Improved verification of commitment transaction confirmation --- pkg/tbtcpg/moving_funds.go | 53 ++++++++++++++++++--------------- pkg/tbtcpg/moving_funds_test.go | 1 + 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 4851b752a1..f5e96cd34b 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -6,7 +6,6 @@ import ( "sort" "github.com/ipfs/go-log/v2" - "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/tbtc" @@ -182,6 +181,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( } err = mft.SubmitMovingFundsCommitment( + taskLogger, walletPublicKeyHash, walletMainUtxo, walletMemberIDs, @@ -436,6 +436,7 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( // SubmitMovingFundsCommitment submits the moving funds commitment and waits // until the transaction has entered the Ethereum blockchain. func (mft *MovingFundsTask) SubmitMovingFundsCommitment( + taskLogger log.StandardLogger, walletPublicKeyHash [20]byte, walletMainUTXO *bitcoin.UnspentTransactionOutput, walletMembersIDs []uint32, @@ -466,38 +467,42 @@ func (mft *MovingFundsTask) SubmitMovingFundsCommitment( return fmt.Errorf("error getting current block [%w]", err) } - // To verify the commitment transaction has entered the Ethereum blockchain - // check that the commitment hash is not zero. - stateCheck := func() (bool, error) { + // Make sure the moving funds commitment transaction has been confirmed. + // Give the transaction at most `6` blocks to enter the blockchain. + for blockHeight := currentBlock + 1; blockHeight <= currentBlock+6; blockHeight++ { + err := blockCounter.WaitForBlockHeight(blockHeight) + if err != nil { + return fmt.Errorf("error while waiting for block height [%w]", err) + } + walletData, err := mft.chain.GetWallet(walletPublicKeyHash) if err != nil { - return false, err + return fmt.Errorf("error wile getting wallet chain data [%w]", err) } - return walletData.MovingFundsTargetWalletsCommitmentHash != [32]byte{}, nil - } + // To verify the commitment transaction has entered the Ethereum + // blockchain check that the commitment hash is not zero. + if walletData.MovingFundsTargetWalletsCommitmentHash != [32]byte{} { + taskLogger.Infof( + "the moving funds commitment transaction successfully "+ + "confirmed at block: [%d]", + blockHeight, + ) + return nil + } - // Wait `5` blocks since the current block and perform the transaction state - // check. If the transaction has not entered the blockchain, consider it an - // error. - result, err := ethereum.WaitForBlockConfirmations( - blockCounter, - currentBlock, - 5, - stateCheck, - ) - if err != nil { - return fmt.Errorf( - "error while waiting for transaction confirmation [%w]", - err, + taskLogger.Infof( + "the moving funds commitment transaction still not confirmed at "+ + "block: [%d]", + blockHeight, ) } - if !result { - return ErrTransactionNotIncluded - } + taskLogger.Info( + "failed to verify the moving funds commitment transaction submission", + ) - return nil + return ErrTransactionNotIncluded } // ProposeMovingFunds returns a moving funds proposal. diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index f3f3ffac23..98a348228a 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -470,6 +470,7 @@ func TestMovingFundsAction_SubmitMovingFundsCommitment(t *testing.T) { task := tbtcpg.NewMovingFundsTask(tbtcChain, nil) err := task.SubmitMovingFundsCommitment( + &testutils.MockLogger{}, walletPublicKeyHash, &walletMainUtxo, test.walletMemberIDs, From ab2770493387ac3d082ce5561890213d5a22b254 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 6 Feb 2024 16:11:54 +0100 Subject: [PATCH 44/50] Added docstring for movingFundsProposalValidityBlocks --- pkg/tbtc/moving_funds.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 96c7cf9128..158ac5e8f5 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -11,8 +11,14 @@ import ( ) const ( - // TODO: Determine what the value should be. - movingFundsProposalValidityBlocks = 600 + // movingFundsProposalValidityBlocks determines the moving funds proposal + // validity time expressed in blocks. In other words, this is the worst-case + // time for moving funds during which the wallet is busy and cannot take + // another actions. The value of 650 blocks is roughly 2 hours and 10 + // minutes, assuming 12 seconds per block. It is a slightly longer validity + // time than in case of redemptions as moving funds involves waiting for + // target wallets commitment transaction to be confirmed. + movingFundsProposalValidityBlocks = 650 ) // MovingFundsProposal represents a moving funds proposal issued by a wallet's From 136cf36a451d13d963f137be0cd60b13b76c5622 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 6 Feb 2024 16:18:10 +0100 Subject: [PATCH 45/50] Updated TODO --- pkg/tbtc/moving_funds.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 158ac5e8f5..9982d150c7 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -86,6 +86,9 @@ func (mfa *movingFundsAction) execute() error { // 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. + // Remember to call `validateMovingFundsProposal` twice: before and + // after waiting for confirmations. The second validation is needed + // to ensure the commitment has not been changed during the waiting. return nil } From e4ec1645da815c38ea1f9bd7e1246b17cbf2b1fd Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 7 Feb 2024 14:19:52 +0100 Subject: [PATCH 46/50] Moved creation of proposal after commitment submission --- pkg/tbtcpg/moving_funds.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index f5e96cd34b..f4a2032791 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -154,20 +154,6 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( return nil, false, fmt.Errorf("cannot find target wallets: [%w]", err) } - proposal, err := mft.ProposeMovingFunds( - taskLogger, - walletPublicKeyHash, - walletMainUtxo, - targetWallets, - 0, - ) - if err != nil { - return nil, false, fmt.Errorf( - "cannot prepare moving funds proposal: [%w]", - err, - ) - } - if !commitmentExists { walletMemberIDs, walletMemberIndex, err := mft.GetWalletMembersInfo( request.WalletOperators, @@ -196,6 +182,20 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( } } + proposal, err := mft.ProposeMovingFunds( + taskLogger, + walletPublicKeyHash, + walletMainUtxo, + targetWallets, + 0, + ) + if err != nil { + return nil, false, fmt.Errorf( + "cannot prepare moving funds proposal: [%w]", + err, + ) + } + return proposal, true, nil } From fa17c41a0ce9bc64dafcaae02657241fb9ce4cf1 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 7 Feb 2024 17:01:57 +0100 Subject: [PATCH 47/50] Improved ceiling divide function --- pkg/tbtcpg/moving_funds.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index f4a2032791..5c76686aad 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -254,7 +254,10 @@ func (mft *MovingFundsTask) findNewTargetWallets( // 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 + if x == 0 { + return 0 + } + return 1 + (x-1)/y } min := func(x, y uint64) uint64 { if x < y { From e5d8ec35ab2328bcb289d960512afcaf3b71f63f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 7 Feb 2024 17:58:54 +0100 Subject: [PATCH 48/50] Improved finding task-executing operator position on wallet member lists --- pkg/tbtcpg/chain_test.go | 6 ++---- pkg/tbtcpg/moving_funds.go | 21 +++++++++++++-------- pkg/tbtcpg/moving_funds_test.go | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index f9f21690ea..54cb8b5ff9 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -892,12 +892,10 @@ func (lc *LocalChain) SetOperatorID( defer lc.mutex.Unlock() _, ok := lc.operatorIDs[operatorAddress] - if ok { - return fmt.Errorf("operator already inserted") + if !ok { + lc.operatorIDs[operatorAddress] = operatorID } - lc.operatorIDs[operatorAddress] = operatorID - return nil } diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 5c76686aad..212fe4f20e 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -409,22 +409,27 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( walletOperators []chain.Address, executingOperator chain.Address, ) ([]uint32, uint32, error) { - walletMemberIDs := make([]uint32, 0) walletMemberIndex := 0 + walletMemberIDs := make([]uint32, 0) for index, operatorAddress := range walletOperators { + // If the operator address is the address of the executing operator save + // its position. Note that since the executing operator can control + // multiple wallet members its address can occur on the list multiple + // times. For clarity, we should save the first occurrence on the list, + // i.e. when `walletMemberIndex` still holds the value of `0`. + if operatorAddress == executingOperator && walletMemberIndex == 0 { + // Increment the index by 1 as operator indexing starts at 1, not 0. + // This ensures the operator's position is correctly identified in + // the range [1, walletOperators.length]. + walletMemberIndex = index + 1 + } + operatorID, err := mft.chain.GetOperatorID(operatorAddress) if err != nil { return nil, 0, fmt.Errorf("failed to get operator ID: [%w]", err) } - // Increment the index by 1 as operator indexing starts at 1, not 0. - // This ensures the operator's position is correctly identified in the - // range [1, walletOperators.length]. - if operatorAddress == executingOperator { - walletMemberIndex = index + 1 - } - walletMemberIDs = append(walletMemberIDs, operatorID) } diff --git a/pkg/tbtcpg/moving_funds_test.go b/pkg/tbtcpg/moving_funds_test.go index 98a348228a..ba44594dbf 100644 --- a/pkg/tbtcpg/moving_funds_test.go +++ b/pkg/tbtcpg/moving_funds_test.go @@ -338,13 +338,15 @@ func TestMovingFundsAction_GetWalletMembersInfo(t *testing.T) { }{ "success case": { walletOperators: []operatorInfo{ + // The executing operator controls two wallet members. {"5df232b0348928793658dd05dfc6b05a59d11ae8", 3}, {"dcc895d32b74b34cef2baa6546884fcda65da1e9", 1}, {"28759deda2ea33bd72f68ea2e8f60cd670c2549f", 2}, {"f7891d42f3c61a49e0aed1e31b151877c0905cf7", 4}, + {"28759deda2ea33bd72f68ea2e8f60cd670c2549f", 2}, }, executingOperator: "28759deda2ea33bd72f68ea2e8f60cd670c2549f", - expectedMemberIDs: []uint32{3, 1, 2, 4}, + expectedMemberIDs: []uint32{3, 1, 2, 4, 2}, expectedOperatorPosition: 3, expectedError: nil, }, From 99aeec51cec3591053f56f25dff079518b3ad1d8 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 7 Feb 2024 19:16:04 +0100 Subject: [PATCH 49/50] Added cache of operator address to wallet member IDs --- pkg/tbtcpg/moving_funds.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 212fe4f20e..66549187eb 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -409,6 +409,12 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( walletOperators []chain.Address, executingOperator chain.Address, ) ([]uint32, uint32, error) { + // Cache mapping operator addresses to their wallet member IDs. It helps to + // limit the number of calls to the ETH client if some operator addresses + // occur on the list multiple times. + operatorIDCache := make(map[chain.Address]uint32) + // TODO: Consider adding a global cache at the `ProposalGenerator` level. + walletMemberIndex := 0 walletMemberIDs := make([]uint32, 0) @@ -425,11 +431,18 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( walletMemberIndex = index + 1 } - operatorID, err := mft.chain.GetOperatorID(operatorAddress) - if err != nil { - return nil, 0, fmt.Errorf("failed to get operator ID: [%w]", err) + // Search for the operator address in the cache. Store the operator + // address in the cache if it's not there. + if _, found := operatorIDCache[operatorAddress]; !found { + operatorID, err := mft.chain.GetOperatorID(operatorAddress) + if err != nil { + return nil, 0, fmt.Errorf("failed to get operator ID: [%w]", err) + } + + operatorIDCache[operatorAddress] = operatorID } + operatorID := operatorIDCache[operatorAddress] walletMemberIDs = append(walletMemberIDs, operatorID) } From 4672795127cb506ede724a5d11beef02c379bdd1 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 8 Feb 2024 10:55:17 +0100 Subject: [PATCH 50/50] Refactored operator caching --- pkg/tbtcpg/moving_funds.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 66549187eb..d38942b590 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -433,17 +433,16 @@ func (mft *MovingFundsTask) GetWalletMembersInfo( // Search for the operator address in the cache. Store the operator // address in the cache if it's not there. - if _, found := operatorIDCache[operatorAddress]; !found { + if operatorID, found := operatorIDCache[operatorAddress]; !found { operatorID, err := mft.chain.GetOperatorID(operatorAddress) if err != nil { return nil, 0, fmt.Errorf("failed to get operator ID: [%w]", err) } - operatorIDCache[operatorAddress] = operatorID + walletMemberIDs = append(walletMemberIDs, operatorID) + } else { + walletMemberIDs = append(walletMemberIDs, operatorID) } - - operatorID := operatorIDCache[operatorAddress] - walletMemberIDs = append(walletMemberIDs, operatorID) } // The task executing operator must always be on the wallet operators list.