diff --git a/Makefile b/Makefile index 480c956c9a..a97bde5c3a 100644 --- a/Makefile +++ b/Makefile @@ -248,11 +248,13 @@ start-upgrade-test-light: zetanode-upgrade @echo "--> Starting light upgrade test (no ZetaChain state populating before upgrade)" cd contrib/localnet/ && $(DOCKER) compose -f docker-compose.yml -f docker-compose-upgrade.yml -f docker-compose-upgrade-light.yml up -d -start-localnet: zetanode +start-localnet: zetanode start-localnet-skip-build + +start-localnet-skip-build: @echo "--> Starting localnet" cd contrib/localnet/ && $(DOCKER) compose -f docker-compose.yml -f docker-compose-setup-only.yml up -d -stop-test: +stop-localnet: cd contrib/localnet/ && $(DOCKER) compose down --remove-orphans ############################################################################### diff --git a/changelog.md b/changelog.md index 6b4ac9ea6a..c0c5d4f306 100644 --- a/changelog.md +++ b/changelog.md @@ -62,6 +62,7 @@ * [2329](https://github.com/zeta-chain/node/pull/2329) - fix TODOs in rpc unit tests * [2342](https://github.com/zeta-chain/node/pull/2342) - extend rpc unit tests with testing extension to include synthetic ethereum txs * [2299](https://github.com/zeta-chain/node/pull/2299) - add `zetae2e` command to deploy test contracts +* [2349](https://github.com/zeta-chain/node/pull/2349) - add TestBitcoinDepositRefund and WithdrawBitcoinMultipleTimes E2E tests ### Fixes diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index d791621170..f8ea2d4fba 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -289,6 +289,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestMessagePassingEVMtoZEVMRevertFailName, } bitcoinTests := []string{ + e2etests.TestBitcoinDepositName, + e2etests.TestBitcoinDepositRefundName, e2etests.TestBitcoinWithdrawSegWitName, e2etests.TestBitcoinWithdrawInvalidAddressName, e2etests.TestZetaWithdrawBTCRevertName, @@ -297,6 +299,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { bitcoinAdvancedTests := []string{ e2etests.TestBitcoinWithdrawTaprootName, e2etests.TestBitcoinWithdrawLegacyName, + e2etests.TestBitcoinWithdrawMultipleName, e2etests.TestBitcoinWithdrawP2SHName, e2etests.TestBitcoinWithdrawP2WSHName, e2etests.TestBitcoinWithdrawRestrictedName, diff --git a/cmd/zetae2e/stress.go b/cmd/zetae2e/stress.go index c6a5353fd1..472b01fc50 100644 --- a/cmd/zetae2e/stress.go +++ b/cmd/zetae2e/stress.go @@ -7,6 +7,7 @@ import ( "math/big" "os" "sort" + "sync" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -223,11 +224,25 @@ func StressTest(cmd *cobra.Command, _ []string) { fmt.Println(" 1. Periodically Withdraw ETH from ZEVM to EVM") fmt.Println(" 2. Display Network metrics to monitor performance [Num Pending outbound tx], [Num Trackers]") - e2eTest.WG.Add(2) - go WithdrawCCtx(e2eTest) // Withdraw from ZEVM to EVM - go EchoNetworkMetrics(e2eTest) // Display Network metrics periodically to monitor performance + var wg sync.WaitGroup - e2eTest.WG.Wait() + wg.Add(2) + + go func() { + defer wg.Done() + + // Withdraw from ZEVM to EVM + WithdrawCCtx(e2eTest) + }() + + go func() { + defer wg.Done() + + // Display Network metrics periodically to monitor performance + EchoNetworkMetrics(e2eTest) + }() + + wg.Wait() } // WithdrawCCtx withdraw ETHZRC20 from ZEVM to EVM diff --git a/docs/development/LOCAL_TESTING.md b/docs/development/LOCAL_TESTING.md index 8ba3893a04..d28c44638e 100644 --- a/docs/development/LOCAL_TESTING.md +++ b/docs/development/LOCAL_TESTING.md @@ -88,7 +88,7 @@ $ docker logs -f orchestrator To stop the tests, ```bash -make stop-test +make stop-localnet ``` ### Run monitoring setup diff --git a/e2e/README.md b/e2e/README.md index 9c53af86b3..ee7c0ee1ed 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -56,3 +56,23 @@ zeta_chain_id: "zetachain-1" ``` NOTE: config is in progress, contracts on the zEVM must be added + +## Debugging + +It's possible to debug a single test using Delve debugger. + +1. Make sure delve is installed. `go install github.com/go-delve/delve/cmd/dlv@latest` +2. Configure your IDE to use Delve as the debugger. For Goland, you can do the following: + - Go to "Run" > "Edit Run Configurations" + - Hit "+" > "Go Remote". Keep port as default (`2345`). Toggle "On Disconnect" > "Stop Delve process" +3. Make sure that localnet is running. For a quick start, you can use `make start-localnet-skip-build`. + Networks need some time to generate blocks. +4. Run test as following: `./e2e/scripts/debug.sh my_test_name arg1 arg2 arg_n`. + Example: `./e2e/scripts/debug.sh bitcoin_withdraw_restricted 0.001` +5. Place a breakpoint in the code. +6. Go to the editor's debug panel and hit "Debug" button. + +You can also run an alias of `zetae2e run` like so: +```shell + `./e2e/scripts/run.sh bitcoin_withdraw_restricted 0.001` +``` diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 71edc76c5d..396b725617 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -56,8 +56,10 @@ const ( Test transfer of Bitcoin asset across chains */ TestBitcoinDepositName = "bitcoin_deposit" + TestBitcoinDepositRefundName = "bitcoin_deposit_refund" TestBitcoinWithdrawSegWitName = "bitcoin_withdraw_segwit" TestBitcoinWithdrawTaprootName = "bitcoin_withdraw_taproot" + TestBitcoinWithdrawMultipleName = "bitcoin_withdraw_multiple" TestBitcoinWithdrawLegacyName = "bitcoin_withdraw_legacy" TestBitcoinWithdrawP2WSHName = "bitcoin_withdraw_p2wsh" TestBitcoinWithdrawP2SHName = "bitcoin_withdraw_p2sh" @@ -332,6 +334,13 @@ var AllE2ETests = []runner.E2ETest{ }, TestBitcoinDeposit, ), + runner.NewE2ETest( + TestBitcoinDepositRefundName, + "deposit Bitcoin into ZEVM; expect refund", []runner.ArgDefinition{ + {Description: "amount in btc", DefaultValue: "0.1"}, + }, + TestBitcoinDepositRefund, + ), runner.NewE2ETest( TestBitcoinWithdrawSegWitName, "withdraw BTC from ZEVM to a SegWit address", @@ -359,6 +368,15 @@ var AllE2ETests = []runner.E2ETest{ }, TestBitcoinWithdrawLegacy, ), + runner.NewE2ETest( + TestBitcoinWithdrawMultipleName, + "withdraw BTC from ZEVM multiple times", + []runner.ArgDefinition{ + {Description: "amount", DefaultValue: "0.01"}, + {Description: "times", DefaultValue: "2"}, + }, + WithdrawBitcoinMultipleTimes, + ), runner.NewE2ETest( TestBitcoinWithdrawP2WSHName, "withdraw BTC from ZEVM to a P2WSH address", diff --git a/e2e/e2etests/helper_bitcoin.go b/e2e/e2etests/helper_bitcoin.go deleted file mode 100644 index 09cb4ded96..0000000000 --- a/e2e/e2etests/helper_bitcoin.go +++ /dev/null @@ -1,120 +0,0 @@ -package e2etests - -import ( - "fmt" - "math/big" - "strconv" - - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcutil" - - "github.com/zeta-chain/zetacore/e2e/runner" - "github.com/zeta-chain/zetacore/e2e/utils" - "github.com/zeta-chain/zetacore/pkg/chains" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" -) - -func parseBitcoinWithdrawArgs(r *runner.E2ERunner, args []string, defaultReceiver string) (btcutil.Address, *big.Int) { - // get bitcoin chain id - chainID := r.GetBitcoinChainID() - - // parse receiver address - var err error - var receiver btcutil.Address - if args[0] == "" { - // use the default receiver - receiver, err = chains.DecodeBtcAddress(defaultReceiver, chainID) - if err != nil { - panic("Invalid default receiver address specified for TestBitcoinWithdraw.") - } - } else { - receiver, err = chains.DecodeBtcAddress(args[0], chainID) - if err != nil { - panic("Invalid receiver address specified for TestBitcoinWithdraw.") - } - } - - // parse the withdrawal amount - withdrawalAmount, err := strconv.ParseFloat(args[1], 64) - if err != nil { - panic("Invalid withdrawal amount specified for TestBitcoinWithdraw.") - } - withdrawalAmountSat, err := btcutil.NewAmount(withdrawalAmount) - if err != nil { - panic(err) - } - amount := big.NewInt(int64(withdrawalAmountSat)) - - return receiver, amount -} - -func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) *btcjson.TxRawResult { - tx, err := r.BTCZRC20.Approve( - r.ZEVMAuth, - r.BTCZRC20Addr, - big.NewInt(amount.Int64()*2), - ) // approve more to cover withdraw fee - if err != nil { - panic(err) - } - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - if receipt.Status != 1 { - panic(fmt.Errorf("approve receipt status is not 1")) - } - - // mine blocks if testing on regnet - stop := r.MineBlocksIfLocalBitcoin() - - // withdraw 'amount' of BTC from ZRC20 to BTC address - tx, err = r.BTCZRC20.Withdraw(r.ZEVMAuth, []byte(to.EncodeAddress()), amount) - if err != nil { - panic(err) - } - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - if receipt.Status != 1 { - panic(fmt.Errorf("withdraw receipt status is not 1")) - } - - // mine 10 blocks to confirm the withdraw tx - _, err = r.GenerateToAddressIfLocalBitcoin(10, to) - if err != nil { - panic(err) - } - - // get cctx and check status - cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, receipt.TxHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) - if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_OutboundMined { - panic(fmt.Errorf("cctx status is not OutboundMined")) - } - - // get bitcoin tx according to the outTxHash in cctx - outTxHash := cctx.GetCurrentOutboundParam().Hash - hash, err := chainhash.NewHashFromStr(outTxHash) - if err != nil { - panic(err) - } - - rawTx, err := r.BtcRPCClient.GetRawTransactionVerbose(hash) - if err != nil { - panic(err) - } - r.Logger.Info("raw tx:") - r.Logger.Info(" TxIn: %d", len(rawTx.Vin)) - for idx, txIn := range rawTx.Vin { - r.Logger.Info(" TxIn %d:", idx) - r.Logger.Info(" TxID:Vout: %s:%d", txIn.Txid, txIn.Vout) - r.Logger.Info(" ScriptSig: %s", txIn.ScriptSig.Hex) - } - r.Logger.Info(" TxOut: %d", len(rawTx.Vout)) - for _, txOut := range rawTx.Vout { - r.Logger.Info(" TxOut %d:", txOut.N) - r.Logger.Info(" Value: %.8f", txOut.Value) - r.Logger.Info(" ScriptPubKey: %s", txOut.ScriptPubKey.Hex) - } - - // stop mining - stop() - - return rawTx -} diff --git a/e2e/e2etests/helper_erc20.go b/e2e/e2etests/helper_erc20.go deleted file mode 100644 index fd4630067b..0000000000 --- a/e2e/e2etests/helper_erc20.go +++ /dev/null @@ -1,32 +0,0 @@ -package e2etests - -import ( - ethcommon "github.com/ethereum/go-ethereum/common" - - "github.com/zeta-chain/zetacore/e2e/runner" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" -) - -// verifyTransferAmountFromCCTX verifies the transfer amount from the CCTX on EVM -func verifyTransferAmountFromCCTX(r *runner.E2ERunner, cctx *crosschaintypes.CrossChainTx, amount int64) { - r.Logger.Info("outTx hash %s", cctx.GetCurrentOutboundParam().Hash) - - receipt, err := r.EVMClient.TransactionReceipt( - r.Ctx, - ethcommon.HexToHash(cctx.GetCurrentOutboundParam().Hash), - ) - if err != nil { - panic(err) - } - r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) - for _, log := range receipt.Logs { - event, err := r.ERC20.ParseTransfer(*log) - if err != nil { - continue - } - r.Logger.Info(" logs: from %s, to %s, value %d", event.From.Hex(), event.To.Hex(), event.Value) - if event.Value.Int64() != amount { - panic("value is not correct") - } - } -} diff --git a/e2e/e2etests/helpers.go b/e2e/e2etests/helpers.go new file mode 100644 index 0000000000..5980679b63 --- /dev/null +++ b/e2e/e2etests/helpers.go @@ -0,0 +1,147 @@ +package e2etests + +import ( + "math/big" + "strconv" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcutil" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/e2e/utils" + "github.com/zeta-chain/zetacore/pkg/chains" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) *btcjson.TxRawResult { + tx, err := r.BTCZRC20.Approve( + r.ZEVMAuth, + r.BTCZRC20Addr, + big.NewInt(amount.Int64()*2), + ) // approve more to cover withdraw fee + require.NoError(r, err) + + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + requireReceiptApproved(r, receipt) + + // mine blocks if testing on regnet + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // withdraw 'amount' of BTC from ZRC20 to BTC address + tx, err = r.BTCZRC20.Withdraw(r.ZEVMAuth, []byte(to.EncodeAddress()), amount) + require.NoError(r, err) + + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + requireReceiptApproved(r, receipt) + + // mine 10 blocks to confirm the withdrawal tx + _, err = r.GenerateToAddressIfLocalBitcoin(10, to) + require.NoError(r, err) + + // get cctx and check status + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, receipt.TxHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + requireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + // get bitcoin tx according to the outTxHash in cctx + outTxHash := cctx.GetCurrentOutboundParam().Hash + hash, err := chainhash.NewHashFromStr(outTxHash) + require.NoError(r, err) + + rawTx, err := r.BtcRPCClient.GetRawTransactionVerbose(hash) + require.NoError(r, err) + + r.Logger.Info("raw tx:") + r.Logger.Info(" TxIn: %d", len(rawTx.Vin)) + for idx, txIn := range rawTx.Vin { + r.Logger.Info(" TxIn %d:", idx) + r.Logger.Info(" TxID:Vout: %s:%d", txIn.Txid, txIn.Vout) + r.Logger.Info(" ScriptSig: %s", txIn.ScriptSig.Hex) + } + r.Logger.Info(" TxOut: %d", len(rawTx.Vout)) + for _, txOut := range rawTx.Vout { + r.Logger.Info(" TxOut %d:", txOut.N) + r.Logger.Info(" Value: %.8f", txOut.Value) + r.Logger.Info(" ScriptPubKey: %s", txOut.ScriptPubKey.Hex) + } + + return rawTx +} + +// verifyTransferAmountFromCCTX verifies the transfer amount from the CCTX on EVM +func verifyTransferAmountFromCCTX(r *runner.E2ERunner, cctx *crosschaintypes.CrossChainTx, amount int64) { + r.Logger.Info("outTx hash %s", cctx.GetCurrentOutboundParam().Hash) + + receipt, err := r.EVMClient.TransactionReceipt( + r.Ctx, + ethcommon.HexToHash(cctx.GetCurrentOutboundParam().Hash), + ) + require.NoError(r, err) + + r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) + + for _, log := range receipt.Logs { + event, err := r.ERC20.ParseTransfer(*log) + if err != nil { + continue + } + r.Logger.Info(" logs: from %s, to %s, value %d", event.From.Hex(), event.To.Hex(), event.Value) + require.Equal(r, amount, event.Value.Int64(), "value is not correct") + } +} + +// Parse helpers ==========================================> + +func parseFloat(t require.TestingT, s string) float64 { + f, err := strconv.ParseFloat(s, 64) + require.NoError(t, err, "unable to parse float %q", s) + return f +} + +func parseInt(t require.TestingT, s string) int { + v, err := strconv.Atoi(s) + require.NoError(t, err, "unable to parse int from %q", s) + + return v +} + +// bigIntFromFloat64 takes float64 (e.g. 0.001) that represents btc amount +// and converts it to big.Int for downstream usage. +func btcAmountFromFloat64(t require.TestingT, amount float64) *big.Int { + satoshi, err := btcutil.NewAmount(amount) + require.NoError(t, err) + + return big.NewInt(int64(satoshi)) +} + +func parseBitcoinWithdrawArgs(r *runner.E2ERunner, args []string, defaultReceiver string) (btcutil.Address, *big.Int) { + require.NotEmpty(r, args, "args list is empty") + + receiverRaw := defaultReceiver + if args[0] != "" { + receiverRaw = args[0] + } + + receiver, err := chains.DecodeBtcAddress(receiverRaw, r.GetBitcoinChainID()) + require.NoError(r, err, "unable to decode btc address") + + withdrawalAmount := parseFloat(r, args[1]) + amount := btcAmountFromFloat64(r, withdrawalAmount) + + return receiver, amount +} + +// Testify aliases ==========================================> + +func requireReceiptApproved(t require.TestingT, receipt *ethtypes.Receipt) { + require.Equal(t, ethtypes.ReceiptStatusSuccessful, receipt.Status, "receipt status is not successful") +} + +func requireCCTXStatus(t require.TestingT, cctx *crosschaintypes.CrossChainTx, expected crosschaintypes.CctxStatus) { + require.NotNil(t, cctx.CctxStatus) + require.Equal(t, expected, cctx.CctxStatus.Status, "cctx status is not %q", expected.String()) +} diff --git a/e2e/e2etests/test_bitcoin_deposit_refund.go b/e2e/e2etests/test_bitcoin_deposit_refund.go index ad684e7a90..c17f04ba5d 100644 --- a/e2e/e2etests/test_bitcoin_deposit_refund.go +++ b/e2e/e2etests/test_bitcoin_deposit_refund.go @@ -1,56 +1,66 @@ package e2etests -// DepositBTCRefund ... -// TODO: define e2e test -// https://github.com/zeta-chain/node-private/issues/79 -//func DepositBTCRefund(r *runner.E2ERunner) { -// r.Logger.InfoLoud("Deposit BTC with invalid memo; should be refunded") -// btc := r.BtcRPCClient -// utxos, err := r.BtcRPCClient.ListUnspent() -// if err != nil { -// panic(err) -// } -// spendableAmount := 0.0 -// spendableUTXOs := 0 -// for _, utxo := range utxos { -// if utxo.Spendable { -// spendableAmount += utxo.Amount -// spendableUTXOs++ -// } -// } -// r.Logger.Info("ListUnspent:") -// r.Logger.Info(" spendableAmount: %f", spendableAmount) -// r.Logger.Info(" spendableUTXOs: %d", spendableUTXOs) -// r.Logger.Info("Now sending two txs to TSS address...") -// _, err = r.SendToTSSFromDeployerToDeposit(r.BTCTSSAddress, 1.1, utxos[:2], btc, r.BTCDeployerAddress) -// if err != nil { -// panic(err) -// } -// _, err = r.SendToTSSFromDeployerToDeposit(r.BTCTSSAddress, 0.05, utxos[2:4], btc, r.BTCDeployerAddress) -// if err != nil { -// panic(err) -// } -// -// r.Logger.Info("testing if the deposit into BTC ZRC20 is successful...") -// -// // check if the deposit is successful -// initialBalance, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, r.DeployerAddress) -// if err != nil { -// panic(err) -// } -// for { -// time.Sleep(3 * time.Second) -// balance, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, r.DeployerAddress) -// if err != nil { -// panic(err) -// } -// diff := big.NewInt(0) -// diff.Sub(balance, initialBalance) -// if diff.Cmp(big.NewInt(1.15*btcutil.SatoshiPerBitcoin)) != 0 { -// r.Logger.Info("waiting for BTC balance to show up in ZRC contract... current bal %d", balance) -// } else { -// r.Logger.Info("BTC balance is in ZRC contract! Success") -// break -// } -// } -//} +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/e2e/utils" + "github.com/zeta-chain/zetacore/x/crosschain/types" + zetabitcoin "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" +) + +func TestBitcoinDepositRefund(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given BTC address + r.SetBtcAddress(r.Name, false) + + // Given "Live" BTC network + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // Given amount to send + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + amount += zetabitcoin.DefaultDepositorFee + + // Given a list of UTXOs + utxos, err := r.ListDeployerUTXOs() + require.NoError(r, err) + require.NotEmpty(r, utxos) + + // ACT + // Send BTC to TSS address with a dummy memo + txHash, err := r.SendToTSSFromDeployerWithMemo(amount, utxos, []byte("gibberish-memo")) + require.NoError(r, err) + require.NotEmpty(r, txHash) + + // ASSERT + // Now we want to make sure refund TX is completed. + // Let's check that zetaclient issued a refund on BTC + searchForCrossChainWithBtcRefund := utils.Matches(func(tx types.CrossChainTx) bool { + return tx.GetCctxStatus().Status == types.CctxStatus_Reverted && + len(tx.OutboundParams) == 2 && + tx.OutboundParams[1].Hash != "" + }) + + cctxs := utils.WaitCctxByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient, searchForCrossChainWithBtcRefund) + require.Len(r, cctxs, 1) + + // Pick btc tx hash from the cctx + btcTxHash, err := chainhash.NewHashFromStr(cctxs[0].OutboundParams[1].Hash) + require.NoError(r, err) + + // Query the BTC network to check the refund transaction + refundTx, err := r.BtcRPCClient.GetTransaction(btcTxHash) + require.NoError(r, err, refundTx) + + // Finally, check the refund transaction details + refundTxDetails := refundTx.Details[0] + assert.Equal(r, "receive", refundTxDetails.Category) + assert.Equal(r, r.BTCDeployerAddress.EncodeAddress(), refundTxDetails.Address) + assert.NotEmpty(r, refundTxDetails.Amount) + + r.Logger.Info("Sent %f BTC to TSS with invalid memo, got refund of %f BTC", amount, refundTxDetails.Amount) +} diff --git a/e2e/e2etests/test_bitcoin_withdraw_multiple.go b/e2e/e2etests/test_bitcoin_withdraw_multiple.go index b9271b8b91..e744bbe8c8 100644 --- a/e2e/e2etests/test_bitcoin_withdraw_multiple.go +++ b/e2e/e2etests/test_bitcoin_withdraw_multiple.go @@ -1,96 +1,32 @@ package e2etests -// WithdrawBitcoinMultipleTimes ... -// TODO: complete and uncomment E2E test -// https://github.com/zeta-chain/node-private/issues/79 -//func WithdrawBitcoinMultipleTimes(r *runner.E2ERunner, repeat int64) { -// totalAmount := big.NewInt(int64(0.1 * 1e8)) -// -// // #nosec G701 test - always in range -// amount := big.NewInt(int64(0.1 * 1e8 / float64(repeat))) -// -// // check if the deposit is successful -// BTCZRC20Addr, err := r.SystemContract.GasCoinZRC20ByChainId(&bind.CallOpts{}, big.NewInt(common.BtcRegtestChain.ChainId)) -// if err != nil { -// panic(err) -// } -// r.Logger.Info("BTCZRC20Addr: %s", BTCZRC20Addr.Hex()) -// BTCZRC20, err := zrc20.NewZRC20(BTCZRC20Addr, r.ZEVMClient) -// if err != nil { -// panic(err) -// } -// balance, err := BTCZRC20.BalanceOf(&bind.CallOpts{}, r.DeployerAddress) -// if err != nil { -// panic(err) -// } -// if balance.Cmp(totalAmount) < 0 { -// panic(fmt.Errorf("not enough balance in ZRC20 contract")) -// } -// // approve the ZRC20 contract to spend 1 BTC from the deployer address -// { -// // approve more to cover withdraw fee -// tx, err := BTCZRC20.Approve(r.ZEVMAuth, BTCZRC20Addr, totalAmount.Mul(totalAmount, big.NewInt(100))) -// if err != nil { -// panic(err) -// } -// receipt := config.MustWaitForTxReceipt(r.ZEVMClient, tx, r.Logger) -// r.Logger.Info("approve receipt: status %d", receipt.Status) -// if receipt.Status != 1 { -// panic(fmt.Errorf("approve receipt status is not 1")) -// } -// } -// go func() { -// for { -// time.Sleep(3 * time.Second) -// _, err = r.GenerateToAddressIfLocalBitcoin(1, r.BTCDeployerAddress) -// if err != nil { -// panic(err) -// } -// } -// }() -// // withdraw 0.1 BTC from ZRC20 to BTC address -// for i := int64(0); i < repeat; i++ { -// _, gasFee, err := BTCZRC20.WithdrawGasFee(&bind.CallOpts{}) -// if err != nil { -// panic(err) -// } -// r.Logger.Info("withdraw gas fee: %d", gasFee) -// tx, err := BTCZRC20.Withdraw(r.ZEVMAuth, []byte(r.BTCDeployerAddress.EncodeAddress()), amount) -// if err != nil { -// panic(err) -// } -// receipt := config.MustWaitForTxReceipt(r.ZEVMClient, tx, r.Logger) -// r.Logger.Info("withdraw receipt: status %d", receipt.Status) -// if receipt.Status != 1 { -// panic(fmt.Errorf("withdraw receipt status is not 1")) -// } -// _, err = r.GenerateToAddressIfLocalBitcoin(10, r.BTCDeployerAddress) -// if err != nil { -// panic(err) -// } -// cctx := config.WaitCctxMinedByInTxHash(receipt.TxHash.Hex(), r.CctxClient, r.Logger) -// outTxHash := cctx.GetCurrentOutTxParam().OutboundTxHash -// hash, err := chainhash.NewHashFromStr(outTxHash) -// if err != nil { -// panic(err) -// } -// -// rawTx, err := r.BtcRPCClient.GetRawTransactionVerbose(hash) -// if err != nil { -// panic(err) -// } -// r.Logger.Info("raw tx:") -// r.Logger.Info(" TxIn: %d", len(rawTx.Vin)) -// for idx, txIn := range rawTx.Vin { -// r.Logger.Info(" TxIn %d:", idx) -// r.Logger.Info(" TxID:Vout: %s:%d", txIn.Txid, txIn.Vout) -// r.Logger.Info(" ScriptSig: %s", txIn.ScriptSig.Hex) -// } -// r.Logger.Info(" TxOut: %d", len(rawTx.Vout)) -// for _, txOut := range rawTx.Vout { -// r.Logger.Info(" TxOut %d:", txOut.N) -// r.Logger.Info(" Value: %.8f", txOut.Value) -// r.Logger.Info(" ScriptPubKey: %s", txOut.ScriptPubKey.Hex) -// } -// } -//} +import ( + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/pkg/chains" +) + +const defaultReceiver = "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY" + +func WithdrawBitcoinMultipleTimes(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given amount and repeat count + require.Len(r, args, 2) + var ( + amount = btcAmountFromFloat64(r, parseFloat(r, args[0])) + times = parseInt(r, args[1]) + ) + + // Given BTC address set + r.SetBtcAddress(r.Name, false) + + // Given a receiver + receiver, err := chains.DecodeBtcAddress(defaultReceiver, r.GetBitcoinChainID()) + require.NoError(r, err) + + // ACT + for i := 0; i < times; i++ { + withdrawBTCZRC20(r, receiver, amount) + } +} diff --git a/e2e/runner/balances.go b/e2e/runner/balances.go index 19acfd1509..3e176bcb8d 100644 --- a/e2e/runner/balances.go +++ b/e2e/runner/balances.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/pkg/errors" ) // AccountBalances is a struct that contains the balances of the accounts used in the E2E test @@ -100,20 +101,29 @@ func (runner *E2ERunner) GetBitcoinBalance() (string, error) { return "", fmt.Errorf("failed to decode BTC address: %w", err) } + total, err := runner.GetBitcoinBalanceByAddress(address) + if err != nil { + return "", err + } + + return total.String(), nil +} + +// GetBitcoinBalanceByAddress get btc balance by address. +func (runner *E2ERunner) GetBitcoinBalanceByAddress(address btcutil.Address) (btcutil.Amount, error) { unspentList, err := runner.BtcRPCClient.ListUnspentMinMaxAddresses(1, 9999999, []btcutil.Address{address}) if err != nil { - return "", fmt.Errorf("failed to list unspent: %w", err) + return 0, errors.Wrap(err, "failed to list unspent") } - // calculate total amount - var totalAmount btcutil.Amount + var total btcutil.Amount for _, unspent := range unspentList { if unspent.Spendable { - totalAmount += btcutil.Amount(unspent.Amount * 1e8) + total += btcutil.Amount(unspent.Amount * 1e8) } } - return totalAmount.String(), nil + return total, nil } // PrintAccountBalances shows the account balances of the accounts used in the E2E test diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 493a7a3fd3..d4dc3df15c 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/utils" "github.com/zeta-chain/zetacore/pkg/chains" @@ -186,6 +187,7 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( btcRPC := runner.BtcRPCClient to := runner.BTCTSSAddress btcDeployerAddress := runner.BTCDeployerAddress + require.NotNil(runner, runner.BTCDeployerAddress, "btcDeployerAddress is nil") // prepare inputs inputs := make([]btcjson.TransactionInput, len(inputUTXOs)) @@ -261,13 +263,9 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( } stx, signed, err := btcRPC.SignRawTransactionWithWallet2(tx, inputsForSign) - if err != nil { - panic(err) - } + require.NoError(runner, err) + require.True(runner, signed, "btc transaction is not signed") - if !signed { - panic("btc transaction not signed") - } txid, err := btcRPC.SendRawTransaction(stx, true) if err != nil { panic(err) diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 03d05a0aab..2f2f0e49cc 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -2,6 +2,7 @@ package runner import ( "context" + "os" "sync" "time" @@ -117,7 +118,6 @@ type E2ERunner struct { Ctx context.Context CtxCancel context.CancelFunc Logger *Logger - WG sync.WaitGroup BitcoinParams *chaincfg.Params mutex sync.Mutex } @@ -164,8 +164,6 @@ func NewE2ERunner( BtcRPCClient: btcRPCClient, Logger: logger, - - WG: sync.WaitGroup{}, } for _, opt := range opts { opt(r) @@ -295,3 +293,14 @@ func (runner *E2ERunner) PrintContractAddresses() { runner.Logger.Print("ERC20: %s", runner.ERC20Addr.Hex()) runner.Logger.Print("TestDappEVM: %s", runner.EvmTestDAppAddr.Hex()) } + +// Errorf logs an error message. Mimics the behavior of testing.T.Errorf +func (runner *E2ERunner) Errorf(format string, args ...any) { + runner.Logger.Error(format, args...) +} + +// FailNow implemented to mimic the behavior of testing.T.FailNow +func (runner *E2ERunner) FailNow() { + runner.Logger.Error("Test failed") + os.Exit(1) +} diff --git a/e2e/runner/setup_bitcoin.go b/e2e/runner/setup_bitcoin.go index 15af182915..8e39d0143e 100644 --- a/e2e/runner/setup_bitcoin.go +++ b/e2e/runner/setup_bitcoin.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcutil" + "github.com/stretchr/testify/require" ) func (runner *E2ERunner) SetupBitcoinAccount(initNetwork bool) { @@ -85,10 +86,8 @@ func (runner *E2ERunner) SetBtcAddress(name string, rescan bool) { } if rescan { - err = runner.BtcRPCClient.ImportPrivKeyRescan(privkeyWIF, name, true) - if err != nil { - panic(err) - } + err := runner.BtcRPCClient.ImportPrivKeyRescan(privkeyWIF, name, true) + require.NoError(runner, err, "failed to execute ImportPrivKeyRescan") } runner.BTCDeployerAddress, err = btcutil.NewAddressWitnessPubKeyHash( diff --git a/e2e/scripts/debug.sh b/e2e/scripts/debug.sh new file mode 100755 index 0000000000..9269d3ce4d --- /dev/null +++ b/e2e/scripts/debug.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Exit on any error +set -e + +# Check if dlv is installed +if ! command -v dlv &> /dev/null +then + echo "dlv could not be found, installing..." + go install github.com/go-delve/delve/cmd/dlv@latest +fi + +# Check if at least one argument is provided +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [args...]" + exit 1 +fi + +# Extract the test argument +test=$1 +shift + +dlv_opts="--headless --listen=:2345 --api-version=2 --accept-multiclient" + +# Collect additional arguments +e2e_test_args=$(echo "$@" | tr ' ' ',') +e2e_opts="--config cmd/zetae2e/config/local.yml" + +# Echo commands +# shellcheck disable=SC2086 +set -x +dlv debug ./cmd/zetae2e/ $dlv_opts -- run $test:$e2e_test_args $e2e_opts diff --git a/e2e/scripts/run.sh b/e2e/scripts/run.sh new file mode 100755 index 0000000000..8625f41261 --- /dev/null +++ b/e2e/scripts/run.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Exit on any error +set -e + +# Make sure that dlv is installed! +# go install github.com/go-delve/delve/cmd/dlv@latest + +# Check if at least one argument is provided +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [args...]" + exit 1 +fi + +# Extract the test argument +test=$1 +shift + +# Collect additional arguments +e2e_test_args=$(echo "$@" | tr ' ' ',') +e2e_opts="--config cmd/zetae2e/config/local.yml --verbose" + +# Echo commands +# shellcheck disable=SC2086 +set -x +go run ./cmd/zetae2e/ run $test:$e2e_test_args $e2e_opts diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index 093ff4314f..a641ea9518 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -7,10 +7,15 @@ import ( rpchttp "github.com/cometbft/cometbft/rpc/client/http" coretypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" ) +type CCTXClient = crosschaintypes.QueryClient + const ( FungibleAdminName = "fungibleadmin" @@ -170,6 +175,81 @@ func WaitCCTXMinedByIndex( } } +type WaitOpts func(c *waitConfig) + +// MatchStatus waits for a specific CCTX status. +func MatchStatus(s crosschaintypes.CctxStatus) WaitOpts { + return Matches(func(tx crosschaintypes.CrossChainTx) bool { + return tx.CctxStatus != nil && tx.CctxStatus.Status == s + }) +} + +// Matches adds a filter to WaitCctxByInboundHash that checks cctxs match provided callback. +// ALL cctxs should match this filter. +func Matches(fn func(tx crosschaintypes.CrossChainTx) bool) WaitOpts { + return func(c *waitConfig) { c.matchFunction = fn } +} + +type waitConfig struct { + matchFunction func(tx crosschaintypes.CrossChainTx) bool +} + +// WaitCctxByInboundHash waits until cctx appears by inbound hash. +func WaitCctxByInboundHash( + ctx context.Context, + t require.TestingT, + hash string, + c CCTXClient, + opts ...WaitOpts, +) []crosschaintypes.CrossChainTx { + const tick = time.Millisecond * 200 + + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, DefaultCctxTimeout) + defer cancel() + } + + in := &crosschaintypes.QueryInboundHashToCctxDataRequest{InboundHash: hash} + + var cfg waitConfig + for _, opt := range opts { + opt(&cfg) + } + + matches := func(txs []crosschaintypes.CrossChainTx) bool { + if cfg.matchFunction == nil { + return true + } + + for _, tx := range txs { + if ok := cfg.matchFunction(tx); !ok { + return false + } + } + + return true + } + + for { + out, err := c.InTxHashToCctxData(ctx, in) + statusCode, _ := status.FromError(err) + + switch { + case statusCode.Code() == codes.NotFound: + // expected; let's retry + case err != nil: + require.NoError(t, err, "failed to get cctx by inbound hash: %s", hash) + case len(out.CrossChainTxs) > 0 && matches(out.CrossChainTxs): + return out.CrossChainTxs + case ctx.Err() == nil: + require.NoError(t, err, "failed to get cctx by inbound hash (ctx error): %s", hash) + } + + time.Sleep(tick) + } +} + func IsTerminalStatus(status crosschaintypes.CctxStatus) bool { return status == crosschaintypes.CctxStatus_OutboundMined || status == crosschaintypes.CctxStatus_Aborted || diff --git a/go.mod b/go.mod index 709983e8cc..60a7598f16 100644 --- a/go.mod +++ b/go.mod @@ -347,6 +347,8 @@ replace ( github.com/confio/ics23/go => github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + + github.com/rjeczalik/notify => github.com/rjeczalik/notify v0.9.3 // replace broken goleveldb github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 ) diff --git a/go.sum b/go.sum index ac3eb6cc0b..a5bbf02ee3 100644 --- a/go.sum +++ b/go.sum @@ -1478,8 +1478,8 @@ github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNw github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= -github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= -github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -2023,6 +2023,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=