From 59e5d4a070a126296e39328a796ad7cf5a71e768 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 7 Mar 2024 09:06:28 +0100 Subject: [PATCH] Added unit tests for executing moved funds sweep action --- pkg/tbtc/chain_test.go | 86 +++++++++-- pkg/tbtc/internal/test/marshaling.go | 42 ++--- pkg/tbtc/internal/test/tbtctest.go | 8 +- .../moved_funds_sweep_scenario_0.json | 6 +- .../moved_funds_sweep_scenario_1.json | 6 +- pkg/tbtc/moved_funds_sweep_test.go | 144 +++++++++++++++++- 6 files changed, 243 insertions(+), 49 deletions(-) diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 44f7b805c6..ad988616df 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -69,6 +69,9 @@ type localChain struct { movingFundsProposalValidationsMutex sync.Mutex movingFundsProposalValidations map[[32]byte]bool + movedFundsSweepProposalValidationsMutex sync.Mutex + movedFundsSweepProposalValidations map[[32]byte]bool + heartbeatProposalValidationsMutex sync.Mutex heartbeatProposalValidations map[[16]byte]bool @@ -999,7 +1002,63 @@ func (lc *localChain) ValidateMovedFundsSweepProposal( walletPublicKeyHash [20]byte, proposal *MovedFundsSweepProposal, ) error { - panic("unsupported") + lc.movedFundsSweepProposalValidationsMutex.Lock() + defer lc.movedFundsSweepProposalValidationsMutex.Unlock() + + key, err := buildMovedFundsSweepProposalValidationKey( + walletPublicKeyHash, + proposal, + ) + if err != nil { + return err + } + + result, ok := lc.movedFundsSweepProposalValidations[key] + if !ok { + return fmt.Errorf("validation result unknown") + } + + if !result { + return fmt.Errorf("validation failed") + } + + return nil +} + +func (lc *localChain) setMovedFundsSweepProposalValidationResult( + walletPublicKeyHash [20]byte, + proposal *MovedFundsSweepProposal, + result bool, +) error { + lc.movedFundsSweepProposalValidationsMutex.Lock() + defer lc.movedFundsSweepProposalValidationsMutex.Unlock() + + key, err := buildMovedFundsSweepProposalValidationKey( + walletPublicKeyHash, + proposal, + ) + if err != nil { + return err + } + + lc.movedFundsSweepProposalValidations[key] = result + + return nil +} + +func buildMovedFundsSweepProposalValidationKey( + walletPublicKeyHash [20]byte, + proposal *MovedFundsSweepProposal, +) ([32]byte, error) { + var buffer bytes.Buffer + + buffer.Write(walletPublicKeyHash[:]) + + buffer.Write(proposal.MovingFundsTxHash[:]) + binary.Write(&buffer, binary.BigEndian, proposal.MovingFundsTxOutputIndex) + buffer.Write(proposal.SweepTxFee.Bytes()) + + return sha256.Sum256(buffer.Bytes()), nil } // Connect sets up the local chain. @@ -1030,18 +1089,19 @@ func ConnectWithKey( dkgResultChallengeHandlers: make( map[int]func(submission *DKGResultChallengedEvent), ), - wallets: make(map[[20]byte]*WalletChainData), - blocksByTimestamp: make(map[uint64]uint64), - blocksHashesByNumber: make(map[uint64][32]byte), - pastDepositRevealedEvents: make(map[[32]byte][]*DepositRevealedEvent), - depositSweepProposalValidations: make(map[[32]byte]bool), - pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest), - redemptionProposalValidations: make(map[[32]byte]bool), - movingFundsProposalValidations: make(map[[32]byte]bool), - heartbeatProposalValidations: make(map[[16]byte]bool), - depositRequests: make(map[[32]byte]*DepositChainRequest), - blockCounter: blockCounter, - operatorPrivateKey: operatorPrivateKey, + wallets: make(map[[20]byte]*WalletChainData), + blocksByTimestamp: make(map[uint64]uint64), + blocksHashesByNumber: make(map[uint64][32]byte), + pastDepositRevealedEvents: make(map[[32]byte][]*DepositRevealedEvent), + depositSweepProposalValidations: make(map[[32]byte]bool), + pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest), + redemptionProposalValidations: make(map[[32]byte]bool), + movingFundsProposalValidations: make(map[[32]byte]bool), + movedFundsSweepProposalValidations: make(map[[32]byte]bool), + heartbeatProposalValidations: make(map[[16]byte]bool), + depositRequests: make(map[[32]byte]*DepositChainRequest), + blockCounter: blockCounter, + operatorPrivateKey: operatorPrivateKey, } return localChain diff --git a/pkg/tbtc/internal/test/marshaling.go b/pkg/tbtc/internal/test/marshaling.go index ff1ab10f50..d697c00d30 100644 --- a/pkg/tbtc/internal/test/marshaling.go +++ b/pkg/tbtc/internal/test/marshaling.go @@ -367,17 +367,17 @@ func (mfts *MovingFundsTestScenario) UnmarshalJSON(data []byte) error { // proper MovedFundsSweepTestScenario. func (mfts *MovedFundsSweepTestScenario) UnmarshalJSON(data []byte) error { type movedFundsSweepTestScenario struct { - Title string - WalletPublicKey string - MovedFundsUtxo *utxo - WalletMainUtxo *utxo - InputTransactions []string - Fee int64 - Signatures []signature - ExpectedSigHashes []string - ExpectedMovingFundsTransaction string - ExpectedMovingFundsTransactionHash string - ExpectedMovingFundsTransactionWitnessHash string + Title string + WalletPublicKey string + MovedFundsUtxo *utxo + WalletMainUtxo *utxo + InputTransactions []string + Fee int64 + Signatures []signature + ExpectedSigHashes []string + ExpectedMovedFundsSweepTransaction string + ExpectedMovedFundsSweepTransactionHash string + ExpectedMovedFundsSweepTransactionWitnessHash string } var unmarshaled movedFundsSweepTestScenario @@ -439,27 +439,27 @@ func (mfts *MovedFundsSweepTestScenario) UnmarshalJSON(data []byte) error { ) } - // Unmarshal expected moving funds transaction. - mfts.ExpectedMovingFundsTransaction = new(bitcoin.Transaction) - err = mfts.ExpectedMovingFundsTransaction.Deserialize( - hexToSlice(unmarshaled.ExpectedMovingFundsTransaction), + // Unmarshal expected moved funds sweep transaction. + mfts.ExpectedMovedFundsSweepTransaction = new(bitcoin.Transaction) + err = mfts.ExpectedMovedFundsSweepTransaction.Deserialize( + hexToSlice(unmarshaled.ExpectedMovedFundsSweepTransaction), ) if err != nil { return err } - // Unmarshal expected moving funds transaction hash. - mfts.ExpectedMovingFundsTransactionHash, err = bitcoin.NewHashFromString( - unmarshaled.ExpectedMovingFundsTransactionHash, + // Unmarshal expected moved funds sweep transaction hash. + mfts.ExpectedMovedFundsSweepTransactionHash, err = bitcoin.NewHashFromString( + unmarshaled.ExpectedMovedFundsSweepTransactionHash, bitcoin.ReversedByteOrder, ) if err != nil { return err } - // Unmarshal expected moving funds transaction witness hash. - mfts.ExpectedMovingFundsTransactionWitnessHash, err = bitcoin.NewHashFromString( - unmarshaled.ExpectedMovingFundsTransactionWitnessHash, + // Unmarshal expected moved funds sweep transaction witness hash. + mfts.ExpectedMovedFundsSweepTransactionWitnessHash, err = bitcoin.NewHashFromString( + unmarshaled.ExpectedMovedFundsSweepTransactionWitnessHash, bitcoin.ReversedByteOrder, ) if err != nil { diff --git a/pkg/tbtc/internal/test/tbtctest.go b/pkg/tbtc/internal/test/tbtctest.go index ecd212b867..3ba9eec635 100644 --- a/pkg/tbtc/internal/test/tbtctest.go +++ b/pkg/tbtc/internal/test/tbtctest.go @@ -174,10 +174,10 @@ type MovedFundsSweepTestScenario struct { Fee int64 Signatures []*bitcoin.SignatureContainer - ExpectedSigHashes []*big.Int - ExpectedMovingFundsTransaction *bitcoin.Transaction - ExpectedMovingFundsTransactionHash bitcoin.Hash - ExpectedMovingFundsTransactionWitnessHash bitcoin.Hash + ExpectedSigHashes []*big.Int + ExpectedMovedFundsSweepTransaction *bitcoin.Transaction + ExpectedMovedFundsSweepTransactionHash bitcoin.Hash + ExpectedMovedFundsSweepTransactionWitnessHash bitcoin.Hash } // LoadMovedFundsSweepTestScenarios loads all scenarios related with moved funds diff --git a/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_0.json b/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_0.json index a54f7aaf6a..2c1f95738c 100644 --- a/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_0.json +++ b/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_0.json @@ -22,7 +22,7 @@ "ExpectedSigHashes": [ "1e6e803f3394a17346212fc561acfe5265e5d8f478cf2e1d1a21869686adcbfc" ], - "ExpectedMovingFundsTransaction": "010000000001019b1b33bdd3c44404544991889d63afe6caa875983b705106f1d988251d1459200000000000ffffffff011c700000000000001600148db50eb52063ea9d98b3eac91489a90f738986f6024730440220242dbac95ab8e632cd2791e99d3048b96e6e042bcd902f30fbae7e942a24ea3e02201b7416e6d7d36ea142521eb80e0bc29d118f62ab6b8a64a062cc5812cfdb8c89012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000", - "ExpectedMovingFundsTransactionHash": "a586427d66f8ccca1ed8f7e40a2c82aae99a1f85dfce62ffe2f3657350b6fd84", - "ExpectedMovingFundsTransactionWitnessHash": "c37312351f8c0346c6b878d35571a8015b3bc6dcc035c3535e29b79f389acce4" + "ExpectedMovedFundsSweepTransaction": "010000000001019b1b33bdd3c44404544991889d63afe6caa875983b705106f1d988251d1459200000000000ffffffff011c700000000000001600148db50eb52063ea9d98b3eac91489a90f738986f6024730440220242dbac95ab8e632cd2791e99d3048b96e6e042bcd902f30fbae7e942a24ea3e02201b7416e6d7d36ea142521eb80e0bc29d118f62ab6b8a64a062cc5812cfdb8c89012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000", + "ExpectedMovedFundsSweepTransactionHash": "a586427d66f8ccca1ed8f7e40a2c82aae99a1f85dfce62ffe2f3657350b6fd84", + "ExpectedMovedFundsSweepTransactionWitnessHash": "c37312351f8c0346c6b878d35571a8015b3bc6dcc035c3535e29b79f389acce4" } diff --git a/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_1.json b/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_1.json index cbc44f599e..cd4d9b768b 100644 --- a/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_1.json +++ b/pkg/tbtc/internal/test/testdata/moved_funds_sweep_scenario_1.json @@ -34,7 +34,7 @@ "4fe61de7a1031db9b8e5dfa08cec8b3fb502409ff1f5b9d9ac2da0684ac34e64", "15bb75ef4483c44e072f5f35d6c5fcacd63f904ff9913d31500003dd3781fabf" ], - "ExpectedMovingFundsTransaction": "0100000000010218201d563e43a926f5f9fd4498af5c513a3ea284373308aeda39b0a0d57585780000000000ffffffff84fdb6507365f3e2ff62cedf851f9ae9aa822c0ae4f7d81ecaccf8667d4286a50000000000ffffffff0104a60000000000001600148db50eb52063ea9d98b3eac91489a90f738986f60248304502210089dfa958867b2265d0fc08d996af82a9a731bd972f20e0530d37937f38d9ec1002200cbc820a696b99747aed39aeed5d848367773cf6d4e24aaa12fe2ad714a1ff99012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d902473044022063dc201589b1f7810247eaa569baf5e3dda8717a10e77a2ad95661fef643bdc602203bee4dd0c4a24291523bb7542394df4e6008c0c7ddfdd30c41d55036b32a8999012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000", - "ExpectedMovingFundsTransactionHash": "fc78f52ab4094b5c0bf8a782750c24f31b5db2667425fbddccc29d64f89baf9b", - "ExpectedMovingFundsTransactionWitnessHash": "94bdcc1758967e8f54ff54b9edb96974e900097ee626bdc69743f949c4053226" + "ExpectedMovedFundsSweepTransaction": "0100000000010218201d563e43a926f5f9fd4498af5c513a3ea284373308aeda39b0a0d57585780000000000ffffffff84fdb6507365f3e2ff62cedf851f9ae9aa822c0ae4f7d81ecaccf8667d4286a50000000000ffffffff0104a60000000000001600148db50eb52063ea9d98b3eac91489a90f738986f60248304502210089dfa958867b2265d0fc08d996af82a9a731bd972f20e0530d37937f38d9ec1002200cbc820a696b99747aed39aeed5d848367773cf6d4e24aaa12fe2ad714a1ff99012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d902473044022063dc201589b1f7810247eaa569baf5e3dda8717a10e77a2ad95661fef643bdc602203bee4dd0c4a24291523bb7542394df4e6008c0c7ddfdd30c41d55036b32a8999012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d900000000", + "ExpectedMovedFundsSweepTransactionHash": "fc78f52ab4094b5c0bf8a782750c24f31b5db2667425fbddccc29d64f89baf9b", + "ExpectedMovedFundsSweepTransactionWitnessHash": "94bdcc1758967e8f54ff54b9edb96974e900097ee626bdc69743f949c4053226" } diff --git a/pkg/tbtc/moved_funds_sweep_test.go b/pkg/tbtc/moved_funds_sweep_test.go index ba27823a31..68ae7be032 100644 --- a/pkg/tbtc/moved_funds_sweep_test.go +++ b/pkg/tbtc/moved_funds_sweep_test.go @@ -1,14 +1,148 @@ package tbtc import ( + "context" "fmt" + "math/big" "testing" + "time" + + "github.com/keep-network/keep-core/pkg/tecdsa" "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tbtc/internal/test" ) +// TODO: Think about covering unhappy paths for specific steps of the moved funds sweep action. +func TestMovedFundsSweepAction_Execute(t *testing.T) { + scenarios, err := test.LoadMovedFundsSweepTestScenarios() + if err != nil { + t.Fatal(err) + } + + for _, scenario := range scenarios { + t.Run(scenario.Title, func(t *testing.T) { + hostChain := Connect() + bitcoinChain := newLocalBitcoinChain() + + wallet := wallet{ + // Set only relevant fields. + publicKey: scenario.WalletPublicKey, + } + walletPublicKeyHash := bitcoin.PublicKeyHash(wallet.publicKey) + + // Record the transactions that will serve as moved funds sweep + // transaction's inputs in the Bitcoin local chain. + for _, transaction := range scenario.InputTransactions { + err := bitcoinChain.BroadcastTransaction(transaction) + if err != nil { + t.Fatal(err) + } + } + + // Build the moved funds sweep proposal based on the scenario data. + proposal := &MovedFundsSweepProposal{ + SweepTxFee: big.NewInt(scenario.Fee), + MovingFundsTxHash: scenario.MovedFundsUtxo.Outpoint.TransactionHash, + MovingFundsTxOutputIndex: scenario.MovedFundsUtxo.Outpoint.OutputIndex, + } + + // Choose an arbitrary start block and expiration time. + proposalProcessingStartBlock := uint64(100) + proposalExpiryBlock := proposalProcessingStartBlock + + movedFundsSweepProposalValidityBlocks + + // Simulate the on-chain proposal validation passes with success. + err = hostChain.setMovedFundsSweepProposalValidationResult( + walletPublicKeyHash, + proposal, + true, + ) + if err != nil { + t.Fatal(err) + } + + // Record the wallet main UTXO hash in the local host chain so the + // moved funds sweep action can detect it. + var walletMainUtxoHash [32]byte + if scenario.WalletMainUtxo != nil { + walletMainUtxoHash = hostChain.ComputeMainUtxoHash( + scenario.WalletMainUtxo, + ) + } + hostChain.setWallet(walletPublicKeyHash, &WalletChainData{ + MainUtxoHash: walletMainUtxoHash, + }) + + // Create a signing executor mock instance. + signingExecutor := newMockWalletSigningExecutor() + + // The signatures within the scenario fixture are in the format + // suitable for applying them directly to a Bitcoin transaction. + // However, the signing executor operates on raw tECDSA signatures + // so, we need to unpack them first. + rawSignatures := make([]*tecdsa.Signature, len(scenario.Signatures)) + for i, signature := range scenario.Signatures { + rawSignatures[i] = &tecdsa.Signature{ + R: signature.R, + S: signature.S, + } + } + + // Set up the signing executor mock to return the signatures from + // the test fixture when called with the expected parameters. + // Note that the start block is set based on the proposal + // processing start block as done within the action. + signingExecutor.setSignatures( + scenario.ExpectedSigHashes, + proposalProcessingStartBlock, + rawSignatures, + ) + + action := newMovedFundsSweepAction( + logger.With(), + hostChain, + bitcoinChain, + wallet, + signingExecutor, + proposal, + proposalProcessingStartBlock, + proposalExpiryBlock, + func(ctx context.Context, blockHeight uint64) error { + return nil + }, + ) + + // Modify the default parameters of the action to make + // it possible to execute in the current test environment. + action.broadcastCheckDelay = 1 * time.Second + + err = action.execute() + if err != nil { + t.Fatal(err) + } + + // Action execution that completes without an error is a sign of + // success. However, just in case, make an additional check that + // the expected moved funds sweep transaction was actually + // broadcasted on the local Bitcoin chain. + broadcastedMovedFundsSweepTransaction, err := bitcoinChain.GetTransaction( + scenario.ExpectedMovedFundsSweepTransactionHash, + ) + if err != nil { + t.Fatal(err) + } + + testutils.AssertBytesEqual( + t, + scenario.ExpectedMovedFundsSweepTransaction.Serialize(), + broadcastedMovedFundsSweepTransaction.Serialize(), + ) + }) + } +} + func TestAssembleMovedFundsSweepTransaction(t *testing.T) { scenarios, err := test.LoadMovedFundsSweepTestScenarios() if err != nil { @@ -59,19 +193,19 @@ func TestAssembleMovedFundsSweepTransaction(t *testing.T) { testutils.AssertBytesEqual( t, - scenario.ExpectedMovingFundsTransaction.Serialize(), + scenario.ExpectedMovedFundsSweepTransaction.Serialize(), transaction.Serialize(), ) testutils.AssertStringsEqual( t, - "moving funds transaction hash", - scenario.ExpectedMovingFundsTransactionHash.Hex(bitcoin.InternalByteOrder), + "moved funds sweep transaction hash", + scenario.ExpectedMovedFundsSweepTransactionHash.Hex(bitcoin.InternalByteOrder), transaction.Hash().Hex(bitcoin.InternalByteOrder), ) testutils.AssertStringsEqual( t, - "moving funds transaction witness hash", - scenario.ExpectedMovingFundsTransactionWitnessHash.Hex(bitcoin.InternalByteOrder), + "moved funds sweep transaction witness hash", + scenario.ExpectedMovedFundsSweepTransactionWitnessHash.Hex(bitcoin.InternalByteOrder), transaction.WitnessHash().Hex(bitcoin.InternalByteOrder), ) })