From c2dfb7a615e1f1972631c0eea5a0c057d7a1bb4b Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 22 Mar 2024 00:11:36 -0500 Subject: [PATCH] initial commit to support different types of BTC addresses --- common/address.go | 32 +- common/address_test.go | 248 ++++++++- common/bitcoin/address_taproot.go | 23 +- common/bitcoin/address_taproot_test.go | 2 +- e2e/e2etests/test_bitcoin_withdraw_invalid.go | 2 + e2e/runner/bitcoin.go | 3 +- x/crosschain/keeper/evm_hooks.go | 6 +- x/crosschain/keeper/evm_hooks_test.go | 11 +- zetaclient/bitcoin/bitcoin_client.go | 175 ++++--- zetaclient/bitcoin/bitcoin_client_rpc_test.go | 6 +- zetaclient/bitcoin/bitcoin_client_test.go | 79 +-- zetaclient/bitcoin/bitcoin_signer.go | 128 ++--- zetaclient/bitcoin/bitcoin_signer_test.go | 180 ++++++- zetaclient/bitcoin/bitcoin_test.go | 2 +- zetaclient/bitcoin/inbound_tracker.go | 2 +- zetaclient/bitcoin/txscript.go | 242 +++++++++ zetaclient/bitcoin/txscript_test.go | 475 ++++++++++++++++++ zetaclient/bitcoin/utils.go | 32 -- zetaclient/bitcoin/utils_test.go | 85 ---- zetaclient/compliance/compliance_test.go | 3 +- zetaclient/evm/evm_client_test.go | 2 +- zetaclient/evm/evm_signer_test.go | 39 +- zetaclient/evm/inbounds_test.go | 2 +- .../btc/block_trimmed_8332_831071.json | 108 ++++ ...bf7417cfc8a4d6f277ec11f40cd87319f04aa.json | 51 ++ ...7ad049d49719c3493e6495c88f969bae1c570.json | 50 ++ ...3558981a35a360e3d1262a6675892c91322ca.json | 42 ++ ...ab055490650dbdaa6c2c8e380a7e075958a21.json | 42 ++ ...f7ca10039a66a474f91d23a17896f46e677a7.json | 42 ++ ...e4029ab33a99087fd5328a2331b52ff2ebe5b.json | 42 ++ ...f3552faf77b9d5688699a480261424b4f7e53.json | 73 +++ zetaclient/testutils/stub/tss_signer.go | 19 +- zetaclient/testutils/testdata.go | 63 +-- zetaclient/testutils/testdata_naming.go | 6 + 34 files changed, 1908 insertions(+), 409 deletions(-) create mode 100644 zetaclient/bitcoin/txscript.go create mode 100644 zetaclient/bitcoin/txscript_test.go delete mode 100644 zetaclient/bitcoin/utils_test.go create mode 100644 zetaclient/testdata/btc/block_trimmed_8332_831071.json create mode 100644 zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json create mode 100644 zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json create mode 100644 zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json create mode 100644 zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json create mode 100644 zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json create mode 100644 zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json create mode 100644 zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json diff --git a/common/address.go b/common/address.go index a571f58d45..68185fadad 100644 --- a/common/address.go +++ b/common/address.go @@ -57,6 +57,7 @@ func ConvertRecoverToError(r interface{}) error { } } +// DecodeBtcAddress decodes a BTC address from a given string and chainID func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Address, err error) { defer func() { if r := recover(); r != nil { @@ -74,17 +75,38 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre } // test taproot address type address, err = bitcoin.DecodeTaprootAddress(inputAddress) - if err == nil && address.IsForNet(chainParams) { - return address, nil + if err == nil { + if address.IsForNet(chainParams) { + return address, nil + } + return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name) } - // test taproot address fail; continue other types + // test taproot address failed; continue testing other types: P2WSH, P2WPKH, P2SH, P2PKH address, err = btcutil.DecodeAddress(inputAddress, chainParams) if err != nil { - return nil, fmt.Errorf("decode address failed: %s , for input address %s", err.Error(), inputAddress) + return nil, fmt.Errorf("decode address failed: %s, for input address %s", err.Error(), inputAddress) } ok := address.IsForNet(chainParams) if !ok { - return nil, fmt.Errorf("address is not for network %s", chainParams.Name) + return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name) } return } + +// IsBtcAddressSupported returns true if the given BTC address is supported +func IsBtcAddressSupported(addr btcutil.Address) bool { + switch addr.(type) { + // P2TR address + case *bitcoin.AddressTaproot, + // P2WSH address + *btcutil.AddressWitnessScriptHash, + // P2WPKH address + *btcutil.AddressWitnessPubKeyHash, + // P2SH address + *btcutil.AddressScriptHash, + // P2PKH address + *btcutil.AddressPubKeyHash: + return true + } + return false +} diff --git a/common/address_test.go b/common/address_test.go index 51699f25d0..cc3871c95b 100644 --- a/common/address_test.go +++ b/common/address_test.go @@ -3,7 +3,9 @@ package common import ( "testing" + "github.com/btcsuite/btcutil" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/common/bitcoin" . "gopkg.in/check.v1" ) @@ -54,7 +56,7 @@ func TestDecodeBtcAddress(t *testing.T) { t.Run("non legacy valid address with incorrect params", func(t *testing.T) { _, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcMainnetChain().ChainId) - require.ErrorContains(t, err, "address is not for network mainnet") + require.ErrorContains(t, err, "not for network mainnet") }) t.Run("non legacy valid address with correct params", func(t *testing.T) { _, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcRegtestChain().ChainId) @@ -67,6 +69,248 @@ func TestDecodeBtcAddress(t *testing.T) { }) t.Run("taproot address with incorrect params", func(t *testing.T) { _, err := DecodeBtcAddress("bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c", BtcTestNetChain().ChainId) - require.Error(t, err) + require.ErrorContains(t, err, "not for network testnet") }) } + +func Test_IsBtcAddressSupported_P2TR(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + name: "mainnet taproot address", + addr: "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/24991bd2fdc4f744bf7bbd915d4915925eecebdae249f81e057c0a6ffb700ab9 + name: "testnet taproot address", + addr: "tb1p7qqaucx69xtwkx7vwmhz03xjmzxxpy3hk29y7q06mt3k6a8sehhsu5lacw", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "regtest taproot address", + addr: "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a taproot address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*bitcoin.AddressTaproot) + require.True(t, ok) + + // it should be supported + require.NoError(t, err) + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2WSH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + name: "mainnet P2WSH address", + addr: "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/78fac3f0d4c0174c88d21c4bb1e23a8f007e890c6d2cfa64c97389ead16c51ed + name: "testnet P2WSH address", + addr: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "regtest P2WSH address", + addr: "bcrt1qm9mzhyky4w853ft2ms6dtqdyyu3z2tmrq8jg8xglhyuv0dsxzmgs2f0sqy", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2WSH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressWitnessScriptHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2WPKH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b + name: "mainnet P2WPKH address", + addr: "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/508b4d723c754bad001eae9b7f3c12377d3307bd5b595c27fd8a90089094f0e9 + name: "testnet P2WPKH address", + addr: "tb1q6rufg6myrxurdn0h57d2qhtm9zfmjw2mzcm05q", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "regtest P2WPKH address", + addr: "bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2WPKH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressWitnessPubKeyHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2SH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + name: "mainnet P2SH address", + addr: "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/0c8c8f94817e0288a5273f5c971adaa3cee18a895c3ec8544785dddcd96f3848 + name: "testnet P2SH address 1", + addr: "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/b5e074c5e021fcbd91ea14b1db29dfe5d14e1a6e046039467bf6ada7f8cc01b3 + name: "testnet P2SH address 2", + addr: "2MwbFpRpZWv4zREjbdLB9jVW3Q8xonpVeyE", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "testnet P2SH address 1 should also be supported in regtest", + addr: "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + { + name: "testnet P2SH address 2 should also be supported in regtest", + addr: "2MwbFpRpZWv4zREjbdLB9jVW3Q8xonpVeyE", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2SH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressScriptHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2PKH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + name: "mainnet P2PKH address 1", + addr: "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/1e3974386f071de7f65cabb57346c1a22ec9b3e211a96928a98149673f681237 + name: "testnet P2PKH address 1", + addr: "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/e48459f372727f2253b0ea8c71ded83e8270873b8a044feb3435fc7a799a648f + name: "testnet P2PKH address 2", + addr: "n1gXcqxmzwqHmqmgobe1XXuJaweSu69tZz", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "testnet P2PKH address should also be supported in regtest", + addr: "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + { + name: "testnet P2PKH address should also be supported in regtest", + addr: "n1gXcqxmzwqHmqmgobe1XXuJaweSu69tZz", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2PKH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressPubKeyHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} diff --git a/common/bitcoin/address_taproot.go b/common/bitcoin/address_taproot.go index f349c843dd..e178a8e5a3 100644 --- a/common/bitcoin/address_taproot.go +++ b/common/bitcoin/address_taproot.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil/bech32" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" ) @@ -23,7 +24,7 @@ type AddressTaproot struct { AddressSegWit } -var _ btcutil.Address = AddressTaproot{} +var _ btcutil.Address = &AddressTaproot{} // NewAddressTaproot returns a new AddressTaproot. func NewAddressTaproot(witnessProg []byte, @@ -171,7 +172,7 @@ func decodeSegWitAddress(address string) (string, byte, []byte, error) { // ScriptAddress returns the witness program for this address. // // NOTE: This method is part of the Address interface. -func (a AddressSegWit) ScriptAddress() []byte { +func (a *AddressSegWit) ScriptAddress() []byte { return a.witnessProgram[:] } @@ -179,7 +180,7 @@ func (a AddressSegWit) ScriptAddress() []byte { // bitcoin network. // // NOTE: This method is part of the Address interface. -func (a AddressSegWit) IsForNet(net *chaincfg.Params) bool { +func (a *AddressSegWit) IsForNet(net *chaincfg.Params) bool { return a.hrp == net.Bech32HRPSegwit } @@ -188,19 +189,19 @@ func (a AddressSegWit) IsForNet(net *chaincfg.Params) bool { // can be used as a fmt.Stringer. // // NOTE: This method is part of the Address interface. -func (a AddressSegWit) String() string { +func (a *AddressSegWit) String() string { return a.EncodeAddress() } -func DecodeTaprootAddress(addr string) (AddressTaproot, error) { +func DecodeTaprootAddress(addr string) (*AddressTaproot, error) { hrp, version, program, err := decodeSegWitAddress(addr) if err != nil { - return AddressTaproot{}, err + return nil, err } if version != 1 { - return AddressTaproot{}, errors.New("invalid witness version; taproot address must be version 1") + return nil, errors.New("invalid witness version; taproot address must be version 1") } - return AddressTaproot{ + return &AddressTaproot{ AddressSegWit{ hrp: hrp, witnessVersion: version, @@ -208,3 +209,9 @@ func DecodeTaprootAddress(addr string) (AddressTaproot, error) { }, }, nil } + +// PayToWitnessTaprootScript creates a new script to pay to a version 1 +// (taproot) witness program. The passed hash is expected to be valid. +func PayToWitnessTaprootScript(rawKey []byte) ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(rawKey).Script() +} diff --git a/common/bitcoin/address_taproot_test.go b/common/bitcoin/address_taproot_test.go index 197fa99db2..0082b450eb 100644 --- a/common/bitcoin/address_taproot_test.go +++ b/common/bitcoin/address_taproot_test.go @@ -58,6 +58,7 @@ func TestAddressTaproot(t *testing.T) { // these hex string comes from link // https://mempool.space/tx/41f7cbaaf9a8d378d09ee86de32eebef455225520cb71015cc9a7318fb42e326 witnessProg, err := hex.DecodeString("af06f3d4c726a3b952f2f648d86398af5ddfc3df27aa531d97203987add8db2e") + require.Nil(t, err) addr, err := NewAddressTaproot(witnessProg[:], &chaincfg.MainNetParams) require.Nil(t, err) require.Equal(t, addr.EncodeAddress(), "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c") @@ -71,5 +72,4 @@ func TestAddressTaproot(t *testing.T) { require.Nil(t, err) require.Equal(t, "d07f02a03b555079aff1f07b20c97cbb62479ef0a0e692f6b37b8142d2ee6dcb", hex.EncodeToString(addr.ScriptAddress())) } - } diff --git a/e2e/e2etests/test_bitcoin_withdraw_invalid.go b/e2e/e2etests/test_bitcoin_withdraw_invalid.go index d168826493..311895f568 100644 --- a/e2e/e2etests/test_bitcoin_withdraw_invalid.go +++ b/e2e/e2etests/test_bitcoin_withdraw_invalid.go @@ -54,8 +54,10 @@ func WithdrawToInvalidAddress(r *runner.E2ERunner, amount *big.Int) { } receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) if receipt.Status == 1 { + fmt.Println("WithdrawToInvalidAddress panicing") panic(fmt.Errorf("withdraw receipt status is successful for an invalid BTC address")) } + fmt.Printf("WithdrawToInvalidAddress receipt status: %d\n", receipt.Status) // stop mining stop <- struct{}{} } diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 1aa9c52ea8..e4af60c701 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -266,10 +266,11 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( depositorFee := zetabitcoin.DefaultDepositorFee events := zetabitcoin.FilterAndParseIncomingTx( + btcRPC, []btcjson.TxRawResult{*rawtx}, 0, runner.BTCTSSAddress.EncodeAddress(), - &log.Logger, + log.Logger, runner.BitcoinParams, depositorFee, ) diff --git a/x/crosschain/keeper/evm_hooks.go b/x/crosschain/keeper/evm_hooks.go index 9428865463..5be96df48d 100644 --- a/x/crosschain/keeper/evm_hooks.go +++ b/x/crosschain/keeper/evm_hooks.go @@ -7,7 +7,6 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" - "github.com/btcsuite/btcutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -318,9 +317,8 @@ func ValidateZrc20WithdrawEvent(event *zrc20.ZRC20Withdrawal, chainID int64) err if err != nil { return fmt.Errorf("ParseZRC20WithdrawalEvent: invalid address %s: %s", event.To, err) } - _, ok := addr.(*btcutil.AddressWitnessPubKeyHash) - if !ok { - return fmt.Errorf("ParseZRC20WithdrawalEvent: invalid address %s (not P2WPKH address)", event.To) + if !common.IsBtcAddressSupported(addr) { + return fmt.Errorf("ParseZRC20WithdrawalEvent: unsupported address %s", string(event.To)) } } return nil diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index 9dc5377ab2..344957a117 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -150,15 +150,16 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { btcMainNetWithdrawalEvent, err := crosschainkeeper.ParseZRC20WithdrawalEvent(*sample.GetValidZRC20WithdrawToBTC(t).Logs[3]) require.NoError(t, err) err = crosschainkeeper.ValidateZrc20WithdrawEvent(btcMainNetWithdrawalEvent, common.BtcTestNetChain().ChainId) - require.ErrorContains(t, err, "address is not for network testnet3") + require.ErrorContains(t, err, "invalid address") }) - t.Run("unable to validate a event with an invalid address type", func(t *testing.T) { + t.Run("unable to validate an unsupported address type", func(t *testing.T) { btcMainNetWithdrawalEvent, err := crosschainkeeper.ParseZRC20WithdrawalEvent(*sample.GetValidZRC20WithdrawToBTC(t).Logs[3]) require.NoError(t, err) - btcMainNetWithdrawalEvent.To = []byte("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3") - err = crosschainkeeper.ValidateZrc20WithdrawEvent(btcMainNetWithdrawalEvent, common.BtcTestNetChain().ChainId) - require.ErrorContains(t, err, "decode address failed: unknown address type") + btcMainNetWithdrawalEvent.To = []byte("04b2891ba8cb491828db3ebc8a780d43b169e7b3974114e6e50f9bab6ec" + + "63c2f20f6d31b2025377d05c2a704d3bd799d0d56f3a8543d79a01ab6084a1cb204f260") + err = crosschainkeeper.ValidateZrc20WithdrawEvent(btcMainNetWithdrawalEvent, common.BtcMainnetChain().ChainId) + require.ErrorContains(t, err, "unsupported address") }) } diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index fd894ba201..9cf1a81c52 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -8,7 +8,6 @@ import ( "math/big" "os" "sort" - "strconv" "sync" "sync/atomic" "time" @@ -448,10 +447,11 @@ func (ob *BTCChainClient) observeInTx() error { tssAddress := ob.Tss.BTCAddress() // #nosec G701 always positive inTxs := FilterAndParseIncomingTx( + ob.rpcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, - &ob.logger.WatchInTx, + ob.logger.WatchInTx, ob.netParams, depositorFee, ) @@ -650,10 +650,11 @@ type BTCInTxEvnet struct { // vout0: p2wpkh to the TSS address (targetAddress) // vout1: OP_RETURN memo, base64 encoded func FilterAndParseIncomingTx( + rpcClient interfaces.BTCRPCClient, txs []btcjson.TxRawResult, blockNumber uint64, - targetAddress string, - logger *zerolog.Logger, + tssAddress string, + logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, ) []*BTCInTxEvnet { @@ -662,7 +663,7 @@ func FilterAndParseIncomingTx( if idx == 0 { continue // the first tx is coinbase; we do not process coinbase tx } - inTx, err := GetBtcEvent(tx, targetAddress, blockNumber, logger, netParams, depositorFee) + inTx, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, depositorFee) if err != nil { logger.Error().Err(err).Msgf("FilterAndParseIncomingTx: error getting btc event for tx %s in block %d", tx.Txid, blockNumber) continue @@ -720,11 +721,37 @@ func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { return false } +// GetSenderAddressByVinP2TR get the P2TR sender address from the previous transaction +// Note: this function requires the bitcoin node index the previous transaction +func GetSenderAddressByVinP2TR(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { + // query previous transaction + hash, res, err := GetTxResultByHash(rpcClient, vin.Txid) + if err != nil { + return "", err + } + // query previous transaction raw result + rawResult, err := GetRawTxResult(rpcClient, hash, res) + if err != nil { + return "", err + } + // #nosec G701 - always in range + if len(rawResult.Vout) <= int(vin.Vout) { + return "", fmt.Errorf("vout index %d out of range for tx %s", vin.Vout, vin.Txid) + } + // decode sender address from previous vout script if it is a P2TR script + vout := rawResult.Vout[vin.Vout] + if vout.ScriptPubKey.Type == ScriptTypeP2TR { + return DecodeVoutP2TR(vout, net) + } + return "", nil +} + func GetBtcEvent( + rpcClient interfaces.BTCRPCClient, tx btcjson.TxRawResult, - targetAddress string, + tssAddress string, blockNumber uint64, - logger *zerolog.Logger, + logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, ) (*BTCInTxEvnet, error) { @@ -732,49 +759,27 @@ func GetBtcEvent( var value float64 var memo []byte if len(tx.Vout) >= 2 { - // first vout must to addressed to the targetAddress with p2wpkh scriptPubKey - out := tx.Vout[0] - script := out.ScriptPubKey.Hex - if len(script) == 44 && script[:4] == "0014" { // segwit output: 0x00 + 20 bytes of pubkey hash - hash, err := hex.DecodeString(script[4:]) - if err != nil { - return nil, err - } - wpkhAddress, err := btcutil.NewAddressWitnessPubKeyHash(hash, netParams) - if err != nil { - return nil, err - } - if wpkhAddress.EncodeAddress() != targetAddress { - return nil, nil // irrelevant tx to us, skip - } - // deposit amount has to be no less than the minimum depositor fee - if out.Value < depositorFee { - return nil, fmt.Errorf("btc deposit amount %v in txid %s is less than depositor fee %v", value, tx.Txid, depositorFee) - } - value = out.Value - depositorFee + // 1st vout must to addressed to the tssAddress with p2wpkh scriptPubKey + out0 := tx.Vout[0] + receiver, err := DecodeVoutP2WPKH(out0, netParams) + if err != nil { + return nil, err + } + if receiver != tssAddress { + // skip irrelevant tx to us + return nil, nil + } + // deposit amount has to be no less than the minimum depositor fee + if out0.Value < depositorFee { + return nil, fmt.Errorf("btc deposit amount %v in txid %s is less than depositor fee %v", value, tx.Txid, depositorFee) + } + value = out0.Value - depositorFee - out = tx.Vout[1] - script = out.ScriptPubKey.Hex - if len(script) >= 4 && script[:2] == "6a" { // OP_RETURN - memoSize, err := strconv.ParseInt(script[2:4], 16, 32) - if err != nil { - return nil, errors.Wrapf(err, "error decoding pubkey hash") - } - if int(memoSize) != (len(script)-4)/2 { - return nil, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(script)-4)/2) - } - memoBytes, err := hex.DecodeString(script[4:]) - if err != nil { - logger.Warn().Err(err).Msgf("error hex decoding memo") - return nil, fmt.Errorf("error hex decoding memo: %s", err) - } - if bytes.Equal(memoBytes, []byte(common.DonationMessage)) { - logger.Info().Msgf("donation tx: %s; value %f", tx.Txid, value) - return nil, fmt.Errorf("donation tx: %s; value %f", tx.Txid, value) - } - memo = memoBytes - found = true - } + // 2nd vout must be an OP_RETURN memo + out1 := tx.Vout[1] + memo, found, err = DecodeVoutMemoP2WPKH(out1, tx.Txid) + if err != nil { + return nil, err } } if found { @@ -782,8 +787,7 @@ func GetBtcEvent( var fromAddress string if len(tx.Vin) > 0 { vin := tx.Vin[0] - //log.Info().Msgf("vin: %v", vin.Witness) - if len(vin.Witness) == 2 { + if len(vin.Witness) == 2 { // decode P2WPKH sender address pk := vin.Witness[1] pkBytes, err := hex.DecodeString(pk) if err != nil { @@ -795,11 +799,17 @@ func GetBtcEvent( return nil, errors.Wrapf(err, "error decoding pubkey hash") } fromAddress = addr.EncodeAddress() + } else { // try getting P2TR sender address from previous output + sender, err := GetSenderAddressByVinP2TR(rpcClient, vin, netParams) + if err != nil { + return nil, errors.Wrapf(err, "error getting sender address") + } + fromAddress = sender } } return &BTCInTxEvnet{ FromAddress: fromAddress, - ToAddress: targetAddress, + ToAddress: tssAddress, Value: value, MemoBytes: memo, BlockNumber: blockNumber, @@ -952,7 +962,7 @@ func (ob *BTCChainClient) getOutTxidByNonce(nonce uint64, test bool) (string, er return "", fmt.Errorf("getOutTxidByNonce: cannot find outTx txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := ob.GetTxResultByHash(txid) + _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) if err != nil { return "", errors.Wrapf(err, "getOutTxidByNonce: error getting outTx result for nonce %d hash %s", nonce, txid) } @@ -972,7 +982,7 @@ func (ob *BTCChainClient) findNonceMarkUTXO(nonce uint64, txid string) (int, err if err != nil { ob.logger.ObserveOutTx.Error().Err(err).Msgf("findNonceMarkUTXO: error getting satoshis for utxo %v", utxo) } - if utxo.Address == tssAddress && sats == amount && utxo.TxID == txid { + if utxo.Address == tssAddress && sats == amount && utxo.TxID == txid && utxo.Vout == 0 { ob.logger.ObserveOutTx.Info().Msgf("findNonceMarkUTXO: found nonce-mark utxo with txid %s, amount %d satoshi", utxo.TxID, sats) return i, nil } @@ -1147,7 +1157,7 @@ func (ob *BTCChainClient) observeOutTx() { // Note: if txResult is nil, then inMempool flag should be ignored. func (ob *BTCChainClient) checkIncludedTx(cctx *types.CrossChainTx, txHash string) (*btcjson.GetTransactionResult, bool) { outTxID := ob.GetTxID(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) - hash, getTxResult, err := ob.GetTxResultByHash(txHash) + hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) if err != nil { ob.logger.ObserveOutTx.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -1221,7 +1231,7 @@ func (ob *BTCChainClient) removeIncludedTx(nonce uint64) { func (ob *BTCChainClient) checkTssOutTxResult(cctx *types.CrossChainTx, hash *chainhash.Hash, res *btcjson.GetTransactionResult) error { params := cctx.GetCurrentOutTxParam() nonce := params.OutboundTxTssNonce - rawResult, err := ob.getRawTxResult(hash, res) + rawResult, err := GetRawTxResult(ob.rpcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutTxResult: error GetRawTxResultByHash %s", hash.String()) } @@ -1232,12 +1242,12 @@ func (ob *BTCChainClient) checkTssOutTxResult(cctx *types.CrossChainTx, hash *ch // differentiate between normal and restricted cctx if compliance.IsCctxRestricted(cctx) { - err = ob.checkTSSVoutCancelled(params, rawResult.Vout) + err = ob.checkTSSVoutCancelled(params, rawResult.Vout, ob.chain) if err != nil { return errors.Wrapf(err, "checkTssOutTxResult: invalid TSS Vout in cancelled outTx %s nonce %d", hash, nonce) } } else { - err = ob.checkTSSVout(params, rawResult.Vout) + err = ob.checkTSSVout(params, rawResult.Vout, ob.chain) if err != nil { return errors.Wrapf(err, "checkTssOutTxResult: invalid TSS Vout in outTx %s nonce %d", hash, nonce) } @@ -1245,23 +1255,25 @@ func (ob *BTCChainClient) checkTssOutTxResult(cctx *types.CrossChainTx, hash *ch return nil } -func (ob *BTCChainClient) GetTxResultByHash(txID string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { +// GetTxResultByHash gets the transaction result by hash +func GetTxResultByHash(rpcClient interfaces.BTCRPCClient, txID string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { hash, err := chainhash.NewHashFromStr(txID) if err != nil { return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) } // The Bitcoin node has to be configured to watch TSS address - txResult, err := ob.rpcClient.GetTransaction(hash) + txResult, err := rpcClient.GetTransaction(hash) if err != nil { return nil, nil, errors.Wrapf(err, "GetOutTxByTxHash: error GetTransaction %s", hash.String()) } return hash, txResult, nil } -func (ob *BTCChainClient) getRawTxResult(hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { +// GetRawTxResult gets the raw tx result +func GetRawTxResult(rpcClient interfaces.BTCRPCClient, hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { if res.Confirmations == 0 { // for pending tx, we query the raw tx directly - rawResult, err := ob.rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx + rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx if err != nil { return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetRawTransactionVerbose %s", res.TxID) } @@ -1271,7 +1283,7 @@ func (ob *BTCChainClient) getRawTxResult(hash *chainhash.Hash, res *btcjson.GetT if err != nil { return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) } - block, err := ob.rpcClient.GetBlockVerboseTx(blkHash) + block, err := rpcClient.GetBlockVerboseTx(blkHash) if err != nil { return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) } @@ -1321,7 +1333,7 @@ func (ob *BTCChainClient) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { // - The first output is the nonce-mark // - The second output is the correct payment to recipient // - The third output is the change to TSS (optional) -func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []btcjson.Vout) error { +func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []btcjson.Vout, chain common.Chain) error { // vouts: [nonce-mark, payment to recipient, change to TSS (optional)] if !(len(vouts) == 2 || len(vouts) == 3) { return fmt.Errorf("checkTSSVout: invalid number of vouts: %d", len(vouts)) @@ -1330,14 +1342,20 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b nonce := params.OutboundTxTssNonce tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { - recvAddress, amount, err := DecodeP2WPKHVout(vout, ob.chain) + // decode receiver and amount from vout + receiverExpected := tssAddress + if vout.N == 1 { + // the 2nd output is the payment to recipient + receiverExpected = params.Receiver + } + receiverVout, amount, err := DecodeTSSVout(vout, receiverExpected, chain) if err != nil { - return errors.Wrap(err, "checkTSSVout: error decoding P2WPKH vout") + return err } // 1st vout: nonce-mark if vout.N == 0 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVout: nonce-mark address %s not match TSS address %s", recvAddress, tssAddress) + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVout: nonce-mark address %s not match TSS address %s", receiverVout, tssAddress) } if amount != common.NonceMarkAmount(nonce) { return fmt.Errorf("checkTSSVout: nonce-mark amount %d not match nonce-mark amount %d", amount, common.NonceMarkAmount(nonce)) @@ -1345,8 +1363,8 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b } // 2nd vout: payment to recipient if vout.N == 1 { - if recvAddress != params.Receiver { - return fmt.Errorf("checkTSSVout: output address %s not match params receiver %s", recvAddress, params.Receiver) + if receiverVout != params.Receiver { + return fmt.Errorf("checkTSSVout: output address %s not match params receiver %s", receiverVout, params.Receiver) } // #nosec G701 always positive if uint64(amount) != params.Amount.Uint64() { @@ -1355,8 +1373,8 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b } // 3rd vout: change to TSS (optional) if vout.N == 2 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVout: change address %s not match TSS address %s", recvAddress, tssAddress) + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVout: change address %s not match TSS address %s", receiverVout, tssAddress) } } } @@ -1366,7 +1384,7 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b // checkTSSVoutCancelled vout is valid if: // - The first output is the nonce-mark // - The second output is the change to TSS (optional) -func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, vouts []btcjson.Vout) error { +func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, vouts []btcjson.Vout, chain common.Chain) error { // vouts: [nonce-mark, change to TSS (optional)] if !(len(vouts) == 1 || len(vouts) == 2) { return fmt.Errorf("checkTSSVoutCancelled: invalid number of vouts: %d", len(vouts)) @@ -1375,14 +1393,15 @@ func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, nonce := params.OutboundTxTssNonce tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { - recvAddress, amount, err := DecodeP2WPKHVout(vout, ob.chain) + // decode receiver and amount from vout + receiverVout, amount, err := DecodeTSSVout(vout, tssAddress, chain) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } // 1st vout: nonce-mark if vout.N == 0 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVoutCancelled: nonce-mark address %s not match TSS address %s", recvAddress, tssAddress) + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVoutCancelled: nonce-mark address %s not match TSS address %s", receiverVout, tssAddress) } if amount != common.NonceMarkAmount(nonce) { return fmt.Errorf("checkTSSVoutCancelled: nonce-mark amount %d not match nonce-mark amount %d", amount, common.NonceMarkAmount(nonce)) @@ -1390,8 +1409,8 @@ func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, } // 2nd vout: change to TSS (optional) if vout.N == 2 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVoutCancelled: change address %s not match TSS address %s", recvAddress, tssAddress) + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVoutCancelled: change address %s not match TSS address %s", receiverVout, tssAddress) } } } diff --git a/zetaclient/bitcoin/bitcoin_client_rpc_test.go b/zetaclient/bitcoin/bitcoin_client_rpc_test.go index 0f9acc6a3d..6f0085e460 100644 --- a/zetaclient/bitcoin/bitcoin_client_rpc_test.go +++ b/zetaclient/bitcoin/bitcoin_client_rpc_test.go @@ -146,10 +146,11 @@ func (suite *BitcoinClientTestSuite) Test1() { suite.T().Logf("block txs len %d", len(block.Tx)) inTxs := FilterAndParseIncomingTx( + suite.BitcoinChainClient.rpcClient, block.Tx, uint64(block.Height), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - &log.Logger, + log.Logger, &chaincfg.TestNet3Params, 0.0, ) @@ -183,10 +184,11 @@ func (suite *BitcoinClientTestSuite) Test2() { suite.T().Logf("block txs len %d", len(block.Tx)) inTxs := FilterAndParseIncomingTx( + suite.BitcoinChainClient.rpcClient, block.Tx, uint64(block.Height), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - &log.Logger, + log.Logger, &chaincfg.TestNet3Params, 0.0, ) diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index 9f50f9e608..f3e731c1dd 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -51,13 +51,11 @@ func TestConfirmationThreshold(t *testing.T) { func TestAvgFeeRateBlock828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - err := testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) // https://mempool.space/block/000000000000000000025ca01d2c1094b8fd3bacc5468cc3193ced6a14618c27 var blockMb testutils.MempoolBlock - err = testutils.LoadObjectFromJSONFile(&blockMb, path.Join("../", testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(&blockMb, path.Join("../", testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json")) gasRate, err := CalcBlockAvgFeeRate(&blockVb, &chaincfg.MainNetParams) require.NoError(t, err) @@ -67,8 +65,7 @@ func TestAvgFeeRateBlock828440(t *testing.T) { func TestAvgFeeRateBlock828440Errors(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - err := testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) t.Run("block has no transactions", func(t *testing.T) { emptyVb := btcjson.GetBlockVerboseTxResult{Tx: []btcjson.TxRawResult{}} @@ -154,8 +151,7 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { func TestCalcDepositorFee828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - err := testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) avgGasRate := float64(32.0) // #nosec G701 test - always in range gasRate := int64(avgGasRate * clientcommon.BTCOuttxGasPriceMultiplier) @@ -185,71 +181,81 @@ func TestCalcDepositorFee828440(t *testing.T) { func TestCheckTSSVout(t *testing.T) { // the archived outtx raw result file and cctx file // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chainID := int64(8332) + chain := common.BtcMainnetChain() + chainID := chain.ChainId nonce := uint64(148) // create mainnet mock client btcClient := MockBTCClientMainnet() t.Run("valid TSS vout should pass", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) require.NoError(t, err) }) t.Run("should fail if vout length < 2 or > 3", func(t *testing.T) { - _, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + _, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}) + err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}, chain) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) + err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}, chain) require.ErrorContains(t, err, "invalid number of vouts") }) + t.Run("should fail on invalid TSS vout", func(t *testing.T) { + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) + params := cctx.GetCurrentOutTxParam() + + // invalid TSS vout + rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + require.Error(t, err) + }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the receiver address", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match params receiver") }) t.Run("should fail if vout 1 not match payment amount", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() // not match payment amount rawResult.Vout[1].Value = 0.00011000 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match params amount") }) t.Run("should fail if vout 2 is not to the TSS address", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[2].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match TSS address") }) } @@ -257,7 +263,8 @@ func TestCheckTSSVout(t *testing.T) { func TestCheckTSSVoutCancelled(t *testing.T) { // the archived outtx raw result file and cctx file // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chainID := int64(8332) + chain := common.BtcMainnetChain() + chainID := chain.ChainId nonce := uint64(148) // create mainnet mock client @@ -265,58 +272,58 @@ func TestCheckTSSVoutCancelled(t *testing.T) { t.Run("valid TSS vout should pass", func(t *testing.T) { // remove change vout to simulate cancelled tx - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { - _, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + _, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}) + err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}, chain) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) + err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}, chain) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { // remove change vout to simulate cancelled tx - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { // remove change vout to simulate cancelled tx - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the TSS address", func(t *testing.T) { // remove change vout to simulate cancelled tx - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) require.ErrorContains(t, err, "not match TSS address") }) } diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index f665027f3a..92685319fb 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -92,9 +92,70 @@ func (signer *BTCSigner) GetERC20CustodyAddress() ethcommon.Address { return ethcommon.Address{} } +// AddWithdrawTxOutputs adds the outputs to the withdraw tx +func (signer *BTCSigner) AddWithdrawTxOutputs( + tx *wire.MsgTx, + to btcutil.Address, + total float64, + amount float64, + nonceMark int64, + fees *big.Int, + cancelTx bool, +) error { + // convert withdraw amount to satoshis + amountSatoshis, err := GetSatoshis(amount) + if err != nil { + return err + } + + // calculate remaining btc (the change) to TSS self + remaining := total - amount + remainingSats, err := GetSatoshis(remaining) + if err != nil { + return err + } + remainingSats -= fees.Int64() + remainingSats -= nonceMark + if remainingSats < 0 { + return fmt.Errorf("remainder value is negative: %d", remainingSats) + } else if remainingSats == nonceMark { + signer.logger.Info().Msgf("adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats) + remainingSats-- + } + + // 1st output: the nonce-mark btc to TSS self + tssAddrP2WPKH := signer.tssSigner.BTCAddressWitnessPubkeyHash() + payToSelfScript, err := PayToAddrScript(tssAddrP2WPKH) + if err != nil { + return err + } + txOut1 := wire.NewTxOut(nonceMark, payToSelfScript) + tx.AddTxOut(txOut1) + + // 2nd output: the payment to the recipient + if !cancelTx { + pkScript, err := PayToAddrScript(to) + if err != nil { + return err + } + txOut2 := wire.NewTxOut(amountSatoshis, pkScript) + tx.AddTxOut(txOut2) + } else { + // send the amount to TSS self if tx is cancelled + remainingSats += amountSatoshis + } + + // 3rd output: the remaining btc to TSS self + if remainingSats > 0 { + txOut3 := wire.NewTxOut(remainingSats, payToSelfScript) + tx.AddTxOut(txOut3) + } + return nil +} + // SignWithdrawTx receives utxos sorted by value, amount in BTC, feeRate in BTC per Kb func (signer *BTCSigner) SignWithdrawTx( - to *btcutil.AddressWitnessPubKeyHash, + to btcutil.Address, amount float64, gasPrice *big.Int, sizeLimit uint64, @@ -131,11 +192,6 @@ func (signer *BTCSigner) SignWithdrawTx( tx.AddTxIn(txIn) } - amountSatoshis, err := GetSatoshis(amount) - if err != nil { - return nil, err - } - // size checking // #nosec G701 always positive txSize := EstimateSegWitTxSize(uint64(len(prevOuts)), 3) @@ -157,45 +213,11 @@ func (signer *BTCSigner) SignWithdrawTx( signer.logger.Info().Msgf("bitcoin outTx nonce %d gasPrice %s size %d fees %s consolidated %d utxos of value %v", nonce, gasPrice.String(), txSize, fees.String(), consolidatedUtxo, consolidatedValue) - // calculate remaining btc to TSS self - tssAddrWPKH := signer.tssSigner.BTCAddressWitnessPubkeyHash() - payToSelf, err := PayToWitnessPubKeyHashScript(tssAddrWPKH.WitnessProgram()) - if err != nil { - return nil, err - } - remaining := total - amount - remainingSats, err := GetSatoshis(remaining) + // add tx outputs + err = signer.AddWithdrawTxOutputs(tx, to, total, amount, nonceMark, fees, cancelTx) if err != nil { return nil, err } - remainingSats -= fees.Int64() - remainingSats -= nonceMark - if remainingSats < 0 { - return nil, fmt.Errorf("remainder value is negative: %d", remainingSats) - } else if remainingSats == nonceMark { - signer.logger.Info().Msgf("SignWithdrawTx: adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats) - remainingSats-- - } - - // 1st output: the nonce-mark btc to TSS self - txOut1 := wire.NewTxOut(nonceMark, payToSelf) - tx.AddTxOut(txOut1) - - // 2nd output: the payment to the recipient - if !cancelTx { - pkScript, err := PayToWitnessPubKeyHashScript(to.WitnessProgram()) - if err != nil { - return nil, err - } - txOut2 := wire.NewTxOut(amountSatoshis, pkScript) - tx.AddTxOut(txOut2) - } - - // 3rd output: the remaining btc to TSS self - if remainingSats > 0 { - txOut3 := wire.NewTxOut(remainingSats, payToSelf) - tx.AddTxOut(txOut3) - } // sign the tx sigHashes := txscript.NewTxSigHashes(tx) @@ -311,27 +333,13 @@ func (signer *BTCSigner) TryProcessOutTx( } // Check receiver P2WPKH address - bitcoinNetParams, err := common.BitcoinNetParamsFromChainID(params.ReceiverChainId) - if err != nil { - logger.Error().Err(err).Msgf("cannot get bitcoin net params%v", err) - return - } - addr, err := common.DecodeBtcAddress(params.Receiver, params.ReceiverChainId) + to, err := common.DecodeBtcAddress(params.Receiver, params.ReceiverChainId) if err != nil { logger.Error().Err(err).Msgf("cannot decode address %s ", params.Receiver) return } - if !addr.IsForNet(bitcoinNetParams) { - logger.Error().Msgf( - "address %s is not for network %s", - params.Receiver, - bitcoinNetParams.Name, - ) - return - } - to, ok := addr.(*btcutil.AddressWitnessPubKeyHash) - if err != nil || !ok { - logger.Error().Err(err).Msgf("cannot convert address %s to P2WPKH address", params.Receiver) + if !common.IsBtcAddressSupported(to) { + logger.Error().Msgf("unsupported address %s", params.Receiver) return } amount := float64(params.Amount.Uint64()) / 1e8 @@ -353,7 +361,7 @@ func (signer *BTCSigner) TryProcessOutTx( amount = 0.0 // zero out the amount to cancel the tx } - logger.Info().Msgf("SignWithdrawTx: to %s, value %d sats", addr.EncodeAddress(), params.Amount.Uint64()) + logger.Info().Msgf("SignWithdrawTx: to %s, value %d sats", to.EncodeAddress(), params.Amount.Uint64()) logger.Info().Msgf("using utxos: %v", btcClient.utxos) tx, err := signer.SignWithdrawTx( diff --git a/zetaclient/bitcoin/bitcoin_signer_test.go b/zetaclient/bitcoin/bitcoin_signer_test.go index d5b3e0ca97..3534f481fd 100644 --- a/zetaclient/bitcoin/bitcoin_signer_test.go +++ b/zetaclient/bitcoin/bitcoin_signer_test.go @@ -4,7 +4,9 @@ import ( "encoding/hex" "fmt" "math" + "math/big" "math/rand" + "reflect" "sort" "sync" "testing" @@ -12,6 +14,7 @@ import ( clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" @@ -98,7 +101,7 @@ func (s *BTCSignerSuite) TestP2PH(c *C) { prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) originTx.AddTxIn(txIn) - pkScript, err := txscript.PayToAddrScript(addr) + pkScript, err := PayToAddrScript(addr) c.Assert(err, IsNil) @@ -170,7 +173,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) originTx.AddTxIn(txIn) - pkScript, err := txscript.PayToAddrScript(addr) + pkScript, err := PayToAddrScript(addr) c.Assert(err, IsNil) txOut := wire.NewTxOut(100000000, pkScript) originTx.AddTxOut(txOut) @@ -191,7 +194,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { txOut = wire.NewTxOut(0, nil) redeemTx.AddTxOut(txOut) txSigHashes := txscript.NewTxSigHashes(redeemTx) - pkScript, err = PayToWitnessPubKeyHashScript(addr.WitnessProgram()) + pkScript, err = PayToAddrScript(addr) c.Assert(err, IsNil) { @@ -240,7 +243,7 @@ func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, []b addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) require.Nil(t, err) //fmt.Printf("New address: %s\n", addr.EncodeAddress()) - pkScript, err := PayToWitnessPubKeyHashScript(addr.WitnessProgram()) + pkScript, err := PayToAddrScript(addr) require.Nil(t, err) return privateKey, pkScript } @@ -313,7 +316,7 @@ func signTx(t *testing.T, tx *wire.MsgTx, payerScript []byte, privateKey *btcec. } } -func TestP2WPHSize2In3Out(t *testing.T) { +func TestP2WPKHSize2In3Out(t *testing.T) { // Generate payer/payee private keys and P2WPKH addresss privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) @@ -342,7 +345,7 @@ func TestP2WPHSize2In3Out(t *testing.T) { require.Equal(t, vBytes, outTxBytesMin) } -func TestP2WPHSize21In3Out(t *testing.T) { +func TestP2WPKHSize21In3Out(t *testing.T) { // Generate payer/payee private keys and P2WPKH addresss privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) @@ -370,7 +373,7 @@ func TestP2WPHSize21In3Out(t *testing.T) { } } -func TestP2WPHSizeXIn3Out(t *testing.T) { +func TestP2WPKHSizeXIn3Out(t *testing.T) { // Generate payer/payee private keys and P2WPKH addresss privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) @@ -401,7 +404,7 @@ func TestP2WPHSizeXIn3Out(t *testing.T) { } } -func TestP2WPHSizeBreakdown(t *testing.T) { +func TestP2WPKHSizeBreakdown(t *testing.T) { txSize2In3Out := EstimateSegWitTxSize(2, 3) require.Equal(t, outTxBytesMin, txSize2In3Out) @@ -419,7 +422,7 @@ func TestP2WPHSizeBreakdown(t *testing.T) { require.Equal(t, depositFee, 0.00001360) } -// helper function to create a new BitcoinChainClient +// helper function to create a test BitcoinChainClient func createTestClient(t *testing.T) *BTCChainClient { skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" privateKey, err := crypto.HexToECDSA(skHex) @@ -427,14 +430,18 @@ func createTestClient(t *testing.T) *BTCChainClient { tss := interfaces.TestSigner{ PrivKey: privateKey, } - tssAddress := tss.BTCAddressWitnessPubkeyHash().EncodeAddress() - - // Create BitcoinChainClient - client := &BTCChainClient{ + return &BTCChainClient{ Tss: tss, Mu: &sync.Mutex{}, includedTxResults: make(map[string]*btcjson.GetTransactionResult), } +} + +// helper function to create a test BitcoinChainClient with UTXOs +func createTestClientWithUTXOs(t *testing.T) *BTCChainClient { + // Create BitcoinChainClient + client := createTestClient(t) + tssAddress := client.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() // Create 10 dummy UTXOs (22.44 BTC in total) client.utxos = make([]btcjson.ListUnspentResult, 0, 10) @@ -445,6 +452,139 @@ func createTestClient(t *testing.T) *BTCChainClient { return client } +func TestAddWithdrawTxOutputs(t *testing.T) { + // Create test signer and receiver address + signer, err := NewBTCSigner(config.BTCConfig{}, stub.NewTSSMainnet(), clientcommon.DefaultLoggers(), &metrics.TelemetryServer{}) + require.NoError(t, err) + + // tss address and script + tssAddr := signer.tssSigner.BTCAddressWitnessPubkeyHash() + tssScript, err := PayToAddrScript(tssAddr) + require.NoError(t, err) + fmt.Printf("tss address: %s", tssAddr.EncodeAddress()) + + // receiver addresses + receiver := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" + to, err := common.DecodeBtcAddress(receiver, common.BtcMainnetChain().ChainId) + require.NoError(t, err) + toScript, err := PayToAddrScript(to) + require.NoError(t, err) + + // test cases + tests := []struct { + name string + tx *wire.MsgTx + to btcutil.Address + total float64 + amount float64 + nonce int64 + fees *big.Int + cancelTx bool + fail bool + message string + txout []*wire.TxOut + }{ + { + name: "should add outputs successfully", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 1.00012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 20000000, PkScript: toScript}, + {Value: 80000000, PkScript: tssScript}, + }, + }, + { + name: "should cancel tx successfully", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 1.00012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + cancelTx: true, + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 100000000, PkScript: tssScript}, + }, + }, + { + name: "should fail on invalid amount", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 1.00012000, + amount: -0.5, + fail: true, + }, + { + name: "should fail when total < amount", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.00012000, + amount: 0.2, + fail: true, + }, + { + name: "should fail when total < fees + amount + nonce", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.20011000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: true, + message: "remainder value is negative", + }, + { + name: "should not produce duplicate nonce mark", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.20022000, // 0.2 + fee + nonceMark * 2 + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 20000000, PkScript: toScript}, + {Value: 9999, PkScript: tssScript}, // nonceMark - 1 + }, + }, + { + name: "should fail on invalid to address", + tx: wire.NewMsgTx(wire.TxVersion), + to: nil, + total: 1.00012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := signer.AddWithdrawTxOutputs(tt.tx, tt.to, tt.total, tt.amount, tt.nonce, tt.fees, tt.cancelTx) + if tt.fail { + require.Error(t, err) + if tt.message != "" { + require.Contains(t, err.Error(), tt.message) + } + return + } else { + require.NoError(t, err) + require.True(t, reflect.DeepEqual(tt.txout, tt.tx.TxOut)) + } + }) + } +} + func mineTxNSetNonceMark(ob *BTCChainClient, nonce uint64, txid string, preMarkIndex int) { // Mine transaction outTxID := ob.GetTxID(nonce) @@ -465,7 +605,7 @@ func mineTxNSetNonceMark(ob *BTCChainClient, nonce uint64, txid string, preMarkI } func TestSelectUTXOs(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) dummyTxID := "6e6f71d281146c1fc5c755b35908ee449f26786c84e2ae18f98b268de40b7ec4" // Case1: nonce = 0, bootstrap @@ -557,7 +697,7 @@ func TestUTXOConsolidation(t *testing.T) { dummyTxID := "6e6f71d281146c1fc5c755b35908ee449f26786c84e2ae18f98b268de40b7ec4" t.Run("should not consolidate", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 10, amount = 0.01, nonce = 1, rank = 10 @@ -571,7 +711,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate 1 utxo", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 9, amount = 0.01, nonce = 1, rank = 9 @@ -585,7 +725,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate 3 utxos", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 5, amount = 0.01, nonce = 0, rank = 5 @@ -604,7 +744,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate all utxos using rank 1", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 12, amount = 0.01, nonce = 0, rank = 1 @@ -623,7 +763,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate 3 utxos sparse", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 24105431, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 24105431 // input: utxoCap = 5, amount = 0.13, nonce = 24105432, rank = 5 @@ -641,7 +781,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate all utxos sparse", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 24105431, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 24105431 // input: utxoCap = 12, amount = 0.13, nonce = 24105432, rank = 1 diff --git a/zetaclient/bitcoin/bitcoin_test.go b/zetaclient/bitcoin/bitcoin_test.go index de73756be4..de05dcf543 100644 --- a/zetaclient/bitcoin/bitcoin_test.go +++ b/zetaclient/bitcoin/bitcoin_test.go @@ -109,7 +109,7 @@ func buildTX() (*wire.MsgTx, *txscript.TxSigHashes, int, int64, []byte, *btcec.P txIn := wire.NewTxIn(outpoint, nil, nil) tx.AddTxIn(txIn) - pkScript, err := PayToWitnessPubKeyHashScript(addr.WitnessProgram()) + pkScript, err := PayToAddrScript(addr) if err != nil { return nil, nil, 0, 0, nil, nil, false, err } diff --git a/zetaclient/bitcoin/inbound_tracker.go b/zetaclient/bitcoin/inbound_tracker.go index 3aa83dc031..6d3ab14f21 100644 --- a/zetaclient/bitcoin/inbound_tracker.go +++ b/zetaclient/bitcoin/inbound_tracker.go @@ -75,7 +75,7 @@ func (ob *BTCChainClient) CheckReceiptForBtcTxHash(txHash string, vote bool) (st return "", err } // #nosec G701 always positive - event, err := GetBtcEvent(*tx, tss, uint64(blockVb.Height), &ob.logger.WatchInTx, ob.netParams, depositorFee) + event, err := GetBtcEvent(ob.rpcClient, *tx, tss, uint64(blockVb.Height), ob.logger.WatchInTx, ob.netParams, depositorFee) if err != nil { return "", err } diff --git a/zetaclient/bitcoin/txscript.go b/zetaclient/bitcoin/txscript.go new file mode 100644 index 0000000000..de344c6140 --- /dev/null +++ b/zetaclient/bitcoin/txscript.go @@ -0,0 +1,242 @@ +package bitcoin + +import ( + "bytes" + "encoding/hex" + "fmt" + "strconv" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/cosmos/btcutil/base58" + "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/common/bitcoin" + "golang.org/x/crypto/ripemd160" + + "github.com/pkg/errors" +) + +const ( + // P2TR script type + ScriptTypeP2TR = "witness_v1_taproot" + + // P2WSH script type + ScriptTypeP2WSH = "witness_v0_scripthash" + + // P2WPKH script type + ScriptTypeP2WPKH = "witness_v0_keyhash" + + // P2SH script type + ScriptTypeP2SH = "scripthash" + + // P2PKH script type + ScriptTypeP2PKH = "pubkeyhash" +) + +// PayToAddrScript creates a new script to pay a transaction output to a the +// specified address. +func PayToAddrScript(addr btcutil.Address) ([]byte, error) { + switch addr := addr.(type) { + case *bitcoin.AddressTaproot: + return bitcoin.PayToWitnessTaprootScript(addr.ScriptAddress()) + default: + return txscript.PayToAddrScript(addr) + } +} + +// DecodeVoutP2TR decodes receiver and amount from P2TR output +func DecodeVoutP2TR(vout btcjson.Vout, net *chaincfg.Params) (string, error) { + // check tx script type + if vout.ScriptPubKey.Type != ScriptTypeP2TR { + return "", fmt.Errorf("want scriptPubKey type witness_v1_taproot, got %s", vout.ScriptPubKey.Type) + } + // decode P2TR scriptPubKey [OP_1 0x20 <32-byte-hash>] + scriptPubKey := vout.ScriptPubKey.Hex + decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) + if err != nil { + return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + } + if len(decodedScriptPubKey) != 34 || + decodedScriptPubKey[0] != txscript.OP_1 || + decodedScriptPubKey[1] != 0x20 { + return "", fmt.Errorf("invalid P2TR scriptPubKey: %s", scriptPubKey) + } + witnessProg := decodedScriptPubKey[2:] + receiverAddress, err := bitcoin.NewAddressTaproot(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from scriptPubKey %s", scriptPubKey) + } + return receiverAddress.EncodeAddress(), nil +} + +// DecodeVoutP2WSH decodes receiver and amount from P2WSH output +func DecodeVoutP2WSH(vout btcjson.Vout, net *chaincfg.Params) (string, error) { + // check tx script type + if vout.ScriptPubKey.Type != ScriptTypeP2WSH { + return "", fmt.Errorf("want scriptPubKey type witness_v0_scripthash, got %s", vout.ScriptPubKey.Type) + } + // decode P2WSH scriptPubKey [OP_0 0x20 <32-byte-hash>] + scriptPubKey := vout.ScriptPubKey.Hex + decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) + if err != nil { + return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + } + if len(decodedScriptPubKey) != 34 || + decodedScriptPubKey[0] != txscript.OP_0 || + decodedScriptPubKey[1] != 0x20 { + return "", fmt.Errorf("invalid P2WSH scriptPubKey: %s", scriptPubKey) + } + witnessProg := decodedScriptPubKey[2:] + receiverAddress, err := btcutil.NewAddressWitnessScriptHash(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from scriptPubKey %s", scriptPubKey) + } + return receiverAddress.EncodeAddress(), nil +} + +// DecodeVoutP2WPKH decodes receiver and amount from P2WPKH output +func DecodeVoutP2WPKH(vout btcjson.Vout, net *chaincfg.Params) (string, error) { + // check tx script type + if vout.ScriptPubKey.Type != ScriptTypeP2WPKH { + return "", fmt.Errorf("want scriptPubKey type witness_v0_keyhash, got %s", vout.ScriptPubKey.Type) + } + // decode P2WPKH scriptPubKey [OP_0 0x14 <20-byte-hash>] + scriptPubKey := vout.ScriptPubKey.Hex + decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) + if err != nil { + return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + } + if len(decodedScriptPubKey) != 22 || + decodedScriptPubKey[0] != txscript.OP_0 || + decodedScriptPubKey[1] != 0x14 { + return "", fmt.Errorf("invalid P2WPKH scriptPubKey: %s", scriptPubKey) + } + witnessProg := decodedScriptPubKey[2:] + receiverAddress, err := btcutil.NewAddressWitnessPubKeyHash(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from scriptPubKey %s", scriptPubKey) + } + return receiverAddress.EncodeAddress(), nil +} + +// DecodeVoutP2SH decodes receiver address from P2SH output +func DecodeVoutP2SH(vout btcjson.Vout, net *chaincfg.Params) (string, error) { + // check tx script type + if vout.ScriptPubKey.Type != ScriptTypeP2SH { + return "", fmt.Errorf("want scriptPubKey type scripthash, got %s", vout.ScriptPubKey.Type) + } + // decode P2SH scriptPubKey [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] + scriptPubKey := vout.ScriptPubKey.Hex + decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) + if err != nil { + return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + } + if len(decodedScriptPubKey) != 23 || + decodedScriptPubKey[0] != txscript.OP_HASH160 || + decodedScriptPubKey[1] != 0x14 || + decodedScriptPubKey[22] != txscript.OP_EQUAL { + return "", fmt.Errorf("invalid P2SH scriptPubKey: %s", scriptPubKey) + } + scriptHash := decodedScriptPubKey[2:22] + return EncodeAddress(scriptHash, net.ScriptHashAddrID), nil +} + +// DecodeVoutP2PKH decodes receiver address from P2PKH output +func DecodeVoutP2PKH(vout btcjson.Vout, net *chaincfg.Params) (string, error) { + // check tx script type + if vout.ScriptPubKey.Type != ScriptTypeP2PKH { + return "", fmt.Errorf("want scriptPubKey type pubkeyhash, got %s", vout.ScriptPubKey.Type) + } + // decode P2PKH scriptPubKey [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] + scriptPubKey := vout.ScriptPubKey.Hex + decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) + if err != nil { + return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + } + if len(decodedScriptPubKey) != 25 || + decodedScriptPubKey[0] != txscript.OP_DUP || + decodedScriptPubKey[1] != txscript.OP_HASH160 || + decodedScriptPubKey[2] != 0x14 || + decodedScriptPubKey[23] != txscript.OP_EQUALVERIFY || + decodedScriptPubKey[24] != txscript.OP_CHECKSIG { + return "", fmt.Errorf("invalid P2PKH scriptPubKey: %s", scriptPubKey) + } + pubKeyHash := decodedScriptPubKey[3:23] + return EncodeAddress(pubKeyHash, net.PubKeyHashAddrID), nil +} + +// DecodeVoutMemoP2WPKH decodes memo from P2WPKH output +// returns (memo, found, error) +func DecodeVoutMemoP2WPKH(vout btcjson.Vout, txid string) ([]byte, bool, error) { + script := vout.ScriptPubKey.Hex + if len(script) >= 4 && script[:2] == "6a" { // OP_RETURN + memoSize, err := strconv.ParseInt(script[2:4], 16, 32) + if err != nil { + return nil, false, errors.Wrapf(err, "error decoding memo size: %s", script) + } + if int(memoSize) != (len(script)-4)/2 { + return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(script)-4)/2) + } + memoBytes, err := hex.DecodeString(script[4:]) + if err != nil { + return nil, false, errors.Wrapf(err, "error hex decoding memo: %s", script) + } + if bytes.Equal(memoBytes, []byte(common.DonationMessage)) { + return nil, false, fmt.Errorf("donation tx: %s", txid) + } + return memoBytes, true, nil + } + return nil, false, nil +} + +// EncodeAddress returns a human-readable payment address given a ripemd160 hash +// and netID which encodes the bitcoin network and address type. It is used +// in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address +// encoding. +// Note: this function is a copy of the function in btcutil/address.go +func EncodeAddress(hash160 []byte, netID byte) string { + // Format is 1 byte for a network and address class (i.e. P2PKH vs + // P2SH), 20 bytes for a RIPEMD160 hash, and 4 bytes of checksum. + return base58.CheckEncode(hash160[:ripemd160.Size], netID) +} + +// DecodeTSSVout decodes receiver and amount from a given TSS vout +func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain common.Chain) (string, int64, error) { + // parse amount + amount, err := GetSatoshis(vout.Value) + if err != nil { + return "", 0, errors.Wrap(err, "error getting satoshis") + } + // get btc chain params + chainParams, err := common.GetBTCChainParams(chain.ChainId) + if err != nil { + return "", 0, errors.Wrapf(err, "error GetBTCChainParams for chain %d", chain.ChainId) + } + // decode cctx receiver address + addr, err := common.DecodeBtcAddress(receiverExpected, chain.ChainId) + if err != nil { + return "", 0, errors.Wrapf(err, "error decoding receiver %s", receiverExpected) + } + // parse receiver address from vout + var receiverVout string + switch addr.(type) { + case *bitcoin.AddressTaproot: + receiverVout, err = DecodeVoutP2TR(vout, chainParams) + case *btcutil.AddressWitnessScriptHash: + receiverVout, err = DecodeVoutP2WSH(vout, chainParams) + case *btcutil.AddressWitnessPubKeyHash: + receiverVout, err = DecodeVoutP2WPKH(vout, chainParams) + case *btcutil.AddressScriptHash: + receiverVout, err = DecodeVoutP2SH(vout, chainParams) + case *btcutil.AddressPubKeyHash: + receiverVout, err = DecodeVoutP2PKH(vout, chainParams) + default: + return "", 0, fmt.Errorf("unsupported receiver address type: %T", addr) + } + if err != nil { + return "", 0, errors.Wrap(err, "error decoding TSS vout") + } + return receiverVout, amount, nil +} diff --git a/zetaclient/bitcoin/txscript_test.go b/zetaclient/bitcoin/txscript_test.go new file mode 100644 index 0000000000..57c1ebfac6 --- /dev/null +++ b/zetaclient/bitcoin/txscript_test.go @@ -0,0 +1,475 @@ +package bitcoin + +import ( + "path" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/zetaclient/testutils" +) + +func TestDecodeVoutP2TR(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + chain := common.BtcMainnetChain() + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + require.Len(t, rawResult.Vout, 2) + + // decode vout 0, P2TR + receiver, err := DecodeVoutP2TR(rawResult.Vout[0], net) + require.NoError(t, err) + require.Equal(t, "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", receiver) +} + +func TestDecodeVoutP2TRErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + chain := common.BtcMainnetChain() + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + t.Run("should return error on wrong script type", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Type = "witness_v0_keyhash" // use non-P2TR script type + _, err := DecodeVoutP2TR(invalidVout, net) + require.ErrorContains(t, err, "want scriptPubKey type witness_v1_taproot") + }) + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := DecodeVoutP2TR(invalidVout, net) + require.ErrorContains(t, err, "error decoding scriptPubKey") + }) + t.Run("should return error on wrong script length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 + _, err := DecodeVoutP2TR(invalidVout, net) + require.ErrorContains(t, err, "invalid P2TR scriptPubKey") + }) + t.Run("should return error on invalid OP_1", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_1 '51' to OP_2 '52' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "51", "52", 1) + _, err := DecodeVoutP2TR(invalidVout, net) + require.ErrorContains(t, err, "invalid P2TR scriptPubKey") + }) + t.Run("should return error on wrong hash length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the length '20' to '19' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "5120", "5119", 1) + _, err := DecodeVoutP2TR(invalidVout, net) + require.ErrorContains(t, err, "invalid P2TR scriptPubKey") + }) +} + +func TestDecodeVoutP2WSH(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + chain := common.BtcMainnetChain() + txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + require.Len(t, rawResult.Vout, 1) + + // decode vout 0, P2WSH + receiver, err := DecodeVoutP2WSH(rawResult.Vout[0], net) + require.NoError(t, err) + require.Equal(t, "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", receiver) +} + +func TestDecodeVoutP2WSHErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + chain := common.BtcMainnetChain() + txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + t.Run("should return error on wrong script type", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Type = "witness_v0_keyhash" // use non-P2WSH script type + _, err := DecodeVoutP2WSH(invalidVout, net) + require.ErrorContains(t, err, "want scriptPubKey type witness_v0_scripthash") + }) + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := DecodeVoutP2WSH(invalidVout, net) + require.ErrorContains(t, err, "error decoding scriptPubKey") + }) + t.Run("should return error on wrong script length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 + _, err := DecodeVoutP2WSH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2WSH scriptPubKey") + }) + t.Run("should return error on invalid OP_0", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_0 '00' to OP_1 '51' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "00", "51", 1) + _, err := DecodeVoutP2WSH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2WSH scriptPubKey") + }) + t.Run("should return error on wrong hash length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the length '20' to '19' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0020", "0019", 1) + _, err := DecodeVoutP2WSH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2WSH scriptPubKey") + }) +} + +func TestDecodeP2WPKHVout(t *testing.T) { + // load archived outtx raw result + // https://mempool.space/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 + chain := common.BtcMainnetChain() + nonce := uint64(148) + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + require.Len(t, rawResult.Vout, 3) + + // decode vout 0, nonce mark 148 + receiver, err := DecodeVoutP2WPKH(rawResult.Vout[0], net) + require.NoError(t, err) + require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) + + // decode vout 1, payment 0.00012000 BTC + receiver, err = DecodeVoutP2WPKH(rawResult.Vout[1], net) + require.NoError(t, err) + require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) + + // decode vout 2, change 0.39041489 BTC + receiver, err = DecodeVoutP2WPKH(rawResult.Vout[2], net) + require.NoError(t, err) + require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) +} + +func TestDecodeP2WPKHVoutErrors(t *testing.T) { + // load archived outtx raw result + // https://mempool.space/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 + chain := common.BtcMainnetChain() + nonce := uint64(148) + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + t.Run("should return error on wrong script type", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Type = "scripthash" // use non-P2WPKH script type + _, err := DecodeVoutP2WPKH(invalidVout, net) + require.ErrorContains(t, err, "want scriptPubKey type witness_v0_keyhash") + }) + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := DecodeVoutP2WPKH(invalidVout, net) + require.ErrorContains(t, err, "error decoding scriptPubKey") + }) + t.Run("should return error on wrong script length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 22 + _, err := DecodeVoutP2WPKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2WPKH scriptPubKey") + }) + t.Run("should return error on wrong hash length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the length '14' to '13' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0014", "0013", 1) + _, err := DecodeVoutP2WPKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2WPKH scriptPubKey") + }) +} + +func TestDecodeVoutP2SH(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + chain := common.BtcMainnetChain() + txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + require.Len(t, rawResult.Vout, 2) + + // decode vout 0, P2SH + receiver, err := DecodeVoutP2SH(rawResult.Vout[0], net) + require.NoError(t, err) + require.Equal(t, "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", receiver) +} + +func TestDecodeVoutP2SHErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + chain := common.BtcMainnetChain() + txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + t.Run("should return error on wrong script type", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Type = "witness_v0_keyhash" // use non-P2SH script type + _, err := DecodeVoutP2SH(invalidVout, net) + require.ErrorContains(t, err, "want scriptPubKey type scripthash") + }) + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := DecodeVoutP2SH(invalidVout, net) + require.ErrorContains(t, err, "error decoding scriptPubKey") + }) + t.Run("should return error on wrong script length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 23 + _, err := DecodeVoutP2SH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2SH scriptPubKey") + }) + t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_HASH160 'a9' to OP_HASH256 'aa' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a9", "aa", 1) + _, err := DecodeVoutP2SH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2SH scriptPubKey") + }) + t.Run("should return error on wrong data length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the length '14' to '13' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a914", "a913", 1) + _, err := DecodeVoutP2SH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2SH scriptPubKey") + }) + t.Run("should return error on invalid OP_EQUAL", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "87", "88", 1) + _, err := DecodeVoutP2SH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2SH scriptPubKey") + }) +} + +func TestDecodeVoutP2PKH(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + chain := common.BtcMainnetChain() + txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + require.Len(t, rawResult.Vout, 2) + + // decode vout 0, P2PKH + receiver, err := DecodeVoutP2PKH(rawResult.Vout[0], net) + require.NoError(t, err) + require.Equal(t, "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", receiver) +} + +func TestDecodeVoutP2PKHErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + chain := common.BtcMainnetChain() + txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + t.Run("should return error on wrong script type", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Type = "scripthash" // use non-P2PKH script type + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "want scriptPubKey type pubkeyhash") + }) + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "error decoding scriptPubKey") + }) + t.Run("should return error on wrong script length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "76a914" // 3 bytes, should be 25 + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2PKH scriptPubKey") + }) + t.Run("should return error on invalid OP_DUP", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_DUP '76' to OP_NIP '77' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76", "77", 1) + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2PKH scriptPubKey") + }) + t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_HASH160 'a9' to OP_HASH256 'aa' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a9", "76aa", 1) + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2PKH scriptPubKey") + }) + t.Run("should return error on wrong data length", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the length '14' to '13' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a914", "76a913", 1) + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2PKH scriptPubKey") + }) + t.Run("should return error on invalid OP_EQUALVERIFY", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_EQUALVERIFY '88' to OP_RESERVED1 '89' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "89ac", 1) + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2PKH scriptPubKey") + }) + t.Run("should return error on invalid OP_CHECKSIG", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // modify the OP_CHECKSIG 'ac' to OP_CHECKSIGVERIFY 'ad' + invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "88ad", 1) + _, err := DecodeVoutP2PKH(invalidVout, net) + require.ErrorContains(t, err, "invalid P2PKH scriptPubKey") + }) +} + +func TestDecodeTSSVout(t *testing.T) { + chain := common.BtcMainnetChain() + + t.Run("should decode P2TR vout", func(t *testing.T) { + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(45000), amount) + }) + t.Run("should decode P2WSH vout", func(t *testing.T) { + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + receiverExpected := "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(36557203), amount) + }) + t.Run("should decode P2WPKH vout", func(t *testing.T) { + // https://mempool.space/tx/5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b + txHash := "5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WPKH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + receiverExpected := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(79938), amount) + }) + t.Run("should decode P2SH vout", func(t *testing.T) { + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + receiverExpected := "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(1003881), amount) + }) + t.Run("should decode P2PKH vout", func(t *testing.T) { + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + + receiverExpected := "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(1140000), amount) + }) +} + +func TestDecodeTSSVoutErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + chain := common.BtcMainnetChain() + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" + + t.Run("should return error on invalid amount", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.Value = -0.05 // use negative amount + receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, chain) + require.ErrorContains(t, err, "error getting satoshis") + require.Empty(t, receiver) + require.Zero(t, amount) + }) + t.Run("should return error on invalid btc chain", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // use invalid chain + invalidChain := common.Chain{ChainId: 123} + receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, invalidChain) + require.ErrorContains(t, err, "error GetBTCChainParams") + require.Empty(t, receiver) + require.Zero(t, amount) + }) + t.Run("should return error when invalid receiver passed", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // use testnet params to decode mainnet receiver + wrongChain := common.BtcTestNetChain() + receiver, amount, err := DecodeTSSVout(invalidVout, "bc1qulmx8ej27cj0xe20953cztr2excnmsqvuh0s5c", wrongChain) + require.ErrorContains(t, err, "error decoding receiver") + require.Empty(t, receiver) + require.Zero(t, amount) + }) + t.Run("should return error on decoding failure", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, chain) + require.ErrorContains(t, err, "error decoding TSS vout") + require.Empty(t, receiver) + require.Zero(t, amount) + }) +} diff --git a/zetaclient/bitcoin/utils.go b/zetaclient/bitcoin/utils.go index 33d654fa6c..aa816c9c66 100644 --- a/zetaclient/bitcoin/utils.go +++ b/zetaclient/bitcoin/utils.go @@ -15,7 +15,6 @@ import ( "github.com/zeta-chain/zetacore/common" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" ) @@ -221,34 +220,3 @@ func round(f float64) int64 { // #nosec G701 always in range return int64(f + 0.5) } - -func PayToWitnessPubKeyHashScript(pubKeyHash []byte) ([]byte, error) { - return txscript.NewScriptBuilder().AddOp(txscript.OP_0).AddData(pubKeyHash).Script() -} - -// DecodeP2WPKHVout decodes receiver and amount from P2WPKH output -func DecodeP2WPKHVout(vout btcjson.Vout, chain common.Chain) (string, int64, error) { - amount, err := GetSatoshis(vout.Value) - if err != nil { - return "", 0, errors.Wrap(err, "error getting satoshis") - } - // decode P2WPKH scriptPubKey - scriptPubKey := vout.ScriptPubKey.Hex - decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) - if err != nil { - return "", 0, errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) - } - if len(decodedScriptPubKey) != 22 { // P2WPKH script - return "", 0, fmt.Errorf("unsupported scriptPubKey: %s", scriptPubKey) - } - witnessVersion := decodedScriptPubKey[0] - witnessProgram := decodedScriptPubKey[2:] - if witnessVersion != 0 { - return "", 0, fmt.Errorf("unsupported witness in scriptPubKey %s", scriptPubKey) - } - recvAddress, err := chain.BTCAddressFromWitnessProgram(witnessProgram) - if err != nil { - return "", 0, errors.Wrapf(err, "error getting receiver from witness program %s", witnessProgram) - } - return recvAddress, amount, nil -} diff --git a/zetaclient/bitcoin/utils_test.go b/zetaclient/bitcoin/utils_test.go deleted file mode 100644 index 6ae066f424..0000000000 --- a/zetaclient/bitcoin/utils_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package bitcoin - -import ( - "path" - "testing" - - "github.com/btcsuite/btcd/btcjson" - "github.com/stretchr/testify/require" - "github.com/zeta-chain/zetacore/common" - "github.com/zeta-chain/zetacore/zetaclient/testutils" -) - -func TestDecodeP2WPKHVout(t *testing.T) { - // load archived outtx raw result - // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chain := common.BtcMainnetChain() - nonce := uint64(148) - nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) - - var rawResult btcjson.TxRawResult - err := testutils.LoadObjectFromJSONFile(&rawResult, nameTx) - require.NoError(t, err) - require.Len(t, rawResult.Vout, 3) - - // decode vout 0, nonce mark 148 - receiver, amount, err := DecodeP2WPKHVout(rawResult.Vout[0], chain) - require.NoError(t, err) - require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) - require.Equal(t, common.NonceMarkAmount(nonce), amount) - - // decode vout 1, payment 0.00012000 BTC - receiver, amount, err = DecodeP2WPKHVout(rawResult.Vout[1], chain) - require.NoError(t, err) - require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) - require.Equal(t, int64(12000), amount) - - // decode vout 2, change 0.39041489 BTC - receiver, amount, err = DecodeP2WPKHVout(rawResult.Vout[2], chain) - require.NoError(t, err) - require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) - require.Equal(t, int64(39041489), amount) -} - -func TestDecodeP2WPKHVoutErrors(t *testing.T) { - // load archived outtx raw result - // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chain := common.BtcMainnetChain() - nonce := uint64(148) - nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) - - var rawResult btcjson.TxRawResult - err := testutils.LoadObjectFromJSONFile(&rawResult, nameTx) - require.NoError(t, err) - - t.Run("should return error on invalid amount", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - invalidVout.Value = -0.5 // negative amount, should not happen - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "error getting satoshis") - }) - t.Run("should return error on invalid script", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - invalidVout.ScriptPubKey.Hex = "invalid script" - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "error decoding scriptPubKey") - }) - t.Run("should return error on unsupported script", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - // can use any invalid script, https://blockstream.info/tx/e95c6ff206103716129c8e3aa8def1427782af3490589d1ea35ccf0122adbc25 (P2SH) - invalidVout.ScriptPubKey.Hex = "a91413b2388e6532653a4b369b7e4ed130f7b81626cc87" - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "unsupported scriptPubKey") - }) - t.Run("should return error on unsupported witness version", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - // use a fake witness version 1, even if version 0 is the only witness version defined in BIP141 - invalidVout.ScriptPubKey.Hex = "01140c1bfb7d38dff0946fdec5626d51ad58d7e9bc54" - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "unsupported witness in scriptPubKey") - }) -} diff --git a/zetaclient/compliance/compliance_test.go b/zetaclient/compliance/compliance_test.go index 638e6c8ba0..93ce21f785 100644 --- a/zetaclient/compliance/compliance_test.go +++ b/zetaclient/compliance/compliance_test.go @@ -14,8 +14,7 @@ import ( func TestCctxRestricted(t *testing.T) { // load archived cctx var cctx crosschaintypes.CrossChainTx - err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_1_6270.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_1_6270.json")) // create config cfg := config.Config{ diff --git a/zetaclient/evm/evm_client_test.go b/zetaclient/evm/evm_client_test.go index 6f4cb677ed..d00b4ffcfb 100644 --- a/zetaclient/evm/evm_client_test.go +++ b/zetaclient/evm/evm_client_test.go @@ -52,7 +52,7 @@ func TestEVM_CheckTxInclusion(t *testing.T) { // load archived evm block // https://etherscan.io/block/19363323 blockNumber := receipt.BlockNumber.Uint64() - block := testutils.LoadEVMBlock(t, chainID, blockNumber, true) + block := testutils.LoadEVMBlock(chainID, blockNumber, true) // create client blockCache, err := lru.New(1000) diff --git a/zetaclient/evm/evm_signer_test.go b/zetaclient/evm/evm_signer_test.go index 1685f359bb..2ab17a63bb 100644 --- a/zetaclient/evm/evm_signer_test.go +++ b/zetaclient/evm/evm_signer_test.go @@ -64,16 +64,16 @@ func getNewOutTxProcessor() *outtxprocessor.Processor { return outtxprocessor.NewOutTxProcessorManager(logger) } -func getCCTX() (*types.CrossChainTx, error) { +func getCCTX() *types.CrossChainTx { var cctx crosschaintypes.CrossChainTx - err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) - return &cctx, err + testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) + return &cctx } -func getInvalidCCTX() (*types.CrossChainTx, error) { +func getInvalidCCTX() *types.CrossChainTx { var cctx crosschaintypes.CrossChainTx - err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) - return &cctx, err + testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) + return &cctx } func TestSigner_SetGetConnectorAddress(t *testing.T) { @@ -103,8 +103,7 @@ func TestSigner_SetGetERC20CustodyAddress(t *testing.T) { func TestSigner_TryProcessOutTx(t *testing.T) { evmSigner, err := getNewEvmSigner() require.NoError(t, err) - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() processorManager := getNewOutTxProcessor() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) @@ -123,8 +122,7 @@ func TestSigner_SignOutboundTx(t *testing.T) { // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -153,8 +151,7 @@ func TestSigner_SignRevertTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -183,8 +180,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -213,8 +209,7 @@ func TestSigner_SignCommandTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -261,8 +256,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -291,8 +285,7 @@ func TestSigner_BroadcastOutTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -322,8 +315,7 @@ func TestSigner_getEVMRPC(t *testing.T) { } func TestSigner_SignerErrorMsg(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() msg := SignerErrorMsg(cctx) require.Contains(t, msg, "nonce 68270 chain 56") @@ -335,8 +327,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) diff --git a/zetaclient/evm/inbounds_test.go b/zetaclient/evm/inbounds_test.go index e382340590..fdac9770a0 100644 --- a/zetaclient/evm/inbounds_test.go +++ b/zetaclient/evm/inbounds_test.go @@ -371,7 +371,7 @@ func TestEVM_ObserveTSSReceiveInBlock(t *testing.T) { // load archived evm block // https://etherscan.io/block/19363323 blockNumber := receipt.BlockNumber.Uint64() - block := testutils.LoadEVMBlock(t, chainID, blockNumber, true) + block := testutils.LoadEVMBlock(chainID, blockNumber, true) // create mock client evmClient := stub.NewMockEvmClient() diff --git a/zetaclient/testdata/btc/block_trimmed_8332_831071.json b/zetaclient/testdata/btc/block_trimmed_8332_831071.json new file mode 100644 index 0000000000..4a662f8263 --- /dev/null +++ b/zetaclient/testdata/btc/block_trimmed_8332_831071.json @@ -0,0 +1,108 @@ +{ + "hash": "0000000000000000000157d7c042e957e5005bdce3b10cf1219c846ff88871f6", + "confirmations": 4636, + "strippedsize": 767728, + "size": 1689876, + "weight": 3993060, + "height": 831071, + "version": 536887296, + "versionHex": "20004000", + "merkleroot": "d2e93c6392c54f1877f4b2f8821af02c551afa4c30a9eac61c4d49c6aa5ad70a", + "tx": [ + { + "hex": "", + "txid": "b350eafdbf61a9f5718410ba2851a5d350d59548608b399e4fc15f4ce593a54a", + "hash": "6b400359ca1f3e33ce049ff411535815f0ebdde0de6aa425b9a8c56b470be096", + "size": 214, + "vsize": 187, + "weight": 748, + "version": 2, + "locktime": 0, + "vin": [ + { + "coinbase": "035fae0c0445ced2652f466f756e6472792055534120506f6f6c202364726f70676f6c642f23714ef33cd5000000000000", + "sequence": 4294967295, + "witness": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ] + } + ], + "vout": [ + { + "value": 6.43696349, + "n": 0, + "scriptPubKey": { + "asm": "0 35f6de260c9f3bdee47524c473a6016c0c055cb9", + "hex": "001435f6de260c9f3bdee47524c473a6016c0c055cb9", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN aa21a9ed4ce95738e15b620273a0ceb3f5e243e6e50b84355c1c602783d77ad11ce54c66", + "hex": "6a24aa21a9ed4ce95738e15b620273a0ceb3f5e243e6e50b84355c1c602783d77ad11ce54c66", + "type": "nulldata" + } + } + ] + }, + { + "hex": "", + "txid": "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867", + "hash": "39f6a6cfe5c8d3f0066b2bf3813f2cc5986dd0a425d5df2cbf9aa298433cf1c2", + "size": 226, + "vsize": 175, + "weight": 700, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "9b5071d68ac8f3a9fd5da76bf1880915ea51f16dfa48e4404d4fe2c5ee5fc574", + "vout": 2, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "9e622c221101926bc0134b564419e385ad1708118442a567d51483558c9035949becc91fc84386fa2b1b11268e035e2e756c7c1e6f248c7690cfad469e0c1412" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "OP_RETURN bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000", + "hex": "6a16bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000", + "type": "nulldata" + } + }, + { + "value": 0.0001085, + "n": 1, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0.0008211, + "n": 2, + "scriptPubKey": { + "asm": "1 34439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82", + "hex": "512034439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82", + "type": "witness_v1_taproot" + } + } + ] + } + ], + "time": 1708314180, + "nonce": 1304717940, + "bits": "170371b1", + "difficulty": 81725299822043.22, + "previousblockhash": "00000000000000000000fa6c7c8d518d28c265e4d93618dc11476a3bd9b2c1c2", + "nextblockhash": "0000000000000000000135e541c901f610309100dd7a078ab94969006e6e20e2" +} diff --git a/zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json b/zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json new file mode 100644 index 0000000000..5445d50411 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json @@ -0,0 +1,51 @@ +{ + "hex": "0200000000010197266596828f8e001991cc881a485eb2172a34c25172599a0bfc32389624d2c50200000000ffffffff031027000000000000160014daaae0d3de9d8fdee31661e61aea828b59be78640000000000000000166a1467ed0bcc4e1256bc2ce87d22e190d63a120114bfdf24010000000000160014d1ec69828aedc54637583e059643aee3f862cd2302473044022047ecada1e409279fe2b714db2b126714b88a67032b1bd1247e935c4b7b71ff50022055a480a97d2dbdd8cf8f7585296e62b538fc735b61d672b4565b9a1df4a8225e0121035ce366bfd01fde742562f7cc5e6ae125ec2bac862d3c3a11d2d70b1b3baa9ae000000000", + "txid": "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa", + "hash": "79988813059cd0c82f9291299d4d6324a3da56c8a6507797c0f2ebf0a13cee56", + "size": 253, + "vsize": 172, + "weight": 685, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697", + "vout": 2, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022047ecada1e409279fe2b714db2b126714b88a67032b1bd1247e935c4b7b71ff50022055a480a97d2dbdd8cf8f7585296e62b538fc735b61d672b4565b9a1df4a8225e01", + "035ce366bfd01fde742562f7cc5e6ae125ec2bac862d3c3a11d2d70b1b3baa9ae0" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0001, + "n": 0, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN 67ed0bcc4e1256bc2ce87d22e190d63a120114bf", + "hex": "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf", + "type": "nulldata" + } + }, + { + "value": 0.00074975, + "n": 2, + "scriptPubKey": { + "asm": "0 d1ec69828aedc54637583e059643aee3f862cd23", + "hex": "0014d1ec69828aedc54637583e059643aee3f862cd23", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json b/zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json new file mode 100644 index 0000000000..ae1b750542 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json @@ -0,0 +1,50 @@ +{ + "hex": "0200000000010167e857b252af502c3c140bd6d6a5b467b7a8badb46ccf1c06378e8f969e818360200000000ffffffff03622a000000000000160014daaae0d3de9d8fdee31661e61aea828b59be78640000000000000000186a16bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000010801000000000022512034439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82014045fae0a2b9fe7e076ca74a77565386a550ea1aed8ee30faa05dd2a7b0eeedb17de1ab8860bb381dbd18f2a179dc9ac5bee4c2d6dd62b791c3bded4aaa0f322c900000000", + "txid": "8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570", + "hash": "12a8f7e7dc414b9fd4de5ff0a154a05709da0ca5a1f51a41be30827110bc0127", + "size": 226, + "vsize": 175, + "weight": 700, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867", + "vout": 2, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "45fae0a2b9fe7e076ca74a77565386a550ea1aed8ee30faa05dd2a7b0eeedb17de1ab8860bb381dbd18f2a179dc9ac5bee4c2d6dd62b791c3bded4aaa0f322c9" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0001085, + "n": 0, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000", + "hex": "6a16bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000", + "type": "nulldata" + } + }, + { + "value": 0.00067585, + "n": 2, + "scriptPubKey": { + "asm": "1 34439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82", + "hex": "512034439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82", + "type": "witness_v1_taproot" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json new file mode 100644 index 0000000000..2aa2635469 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json @@ -0,0 +1,42 @@ +{ + "hex": "01000000000101f163b41b5a56ac3dd741d8695d964c50eb4acda02d08b9e0f9827a4c5d2accad0100000000ffffffff0220651100000000001976a914a386e1676ff60f1a0d1645e1c73d06ab7872467488acb778880000000000160014e7f663e64af624f3654f2d23812c6ac9b13dc00c02473044022071bdf8aeb210418daacf0cb5e4185ed7629a2db94be13746985a356bdcc067ac02203a7dc27af4959e03a5d8ee3100170aafc5710e5cdaaf5d2991a509584eb0a7040121020476f998fae688c3f412755b3b850139995781835f66a62368794f938669350600000000", + "txid": "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca", + "hash": "3de8e19acb8e05fc19aa6196ef4b10d303a107b22a035d97808867aacaea335d", + "size": 225, + "vsize": 144, + "weight": 573, + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "adcc2a5d4c7a82f9e0b9082da0cd4aeb504c965d69d841d73dac565a1bb463f1", + "vout": 1, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022071bdf8aeb210418daacf0cb5e4185ed7629a2db94be13746985a356bdcc067ac02203a7dc27af4959e03a5d8ee3100170aafc5710e5cdaaf5d2991a509584eb0a70401", + "020476f998fae688c3f412755b3b850139995781835f66a62368794f9386693506" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0114, + "n": 0, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 a386e1676ff60f1a0d1645e1c73d06ab78724674 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914a386e1676ff60f1a0d1645e1c73d06ab7872467488ac", + "type": "pubkeyhash" + } + }, + { + "value": 0.08943799, + "n": 1, + "scriptPubKey": { + "asm": "0 e7f663e64af624f3654f2d23812c6ac9b13dc00c", + "hex": "0014e7f663e64af624f3654f2d23812c6ac9b13dc00c", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json new file mode 100644 index 0000000000..a388ad429c --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json @@ -0,0 +1,42 @@ +{ + "hex": "020000000001019481d17b8fb50d38fa5f726b858c2300567620447c93b4d77582ebd8ee286ec70000000000000000800269510f000000000017a91404b8d73fbfeccaea8c253279811e60c5e1d4a9a887ef03160000000000160014811535e6ed76ba320d54bb8f418fcb53d527e2c40247304402200e757ee5c1400ae06a47a558e11540ea7acc75ed366b2acc3301e4a17ebeccdc02204e00c03de16b6c14850cd556c503f1258da7d9f25886b4bc4d4510d182e1ec77012102f9edaa905fbafdacddbbae0fb3cefeb092c5a0dce8a7aeaeb7bd17cf6fa116c900000000", + "txid": "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21", + "hash": "6e3db4ed0fcaad7daff5166615b5c432ba151fb885a603cbb33645fe6f360a86", + "size": 223, + "vsize": 142, + "weight": 565, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "c76e28eed8eb8275d7b4937c4420765600238c856b725ffa380db58f7bd18194", + "vout": 0, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "304402200e757ee5c1400ae06a47a558e11540ea7acc75ed366b2acc3301e4a17ebeccdc02204e00c03de16b6c14850cd556c503f1258da7d9f25886b4bc4d4510d182e1ec7701", + "02f9edaa905fbafdacddbbae0fb3cefeb092c5a0dce8a7aeaeb7bd17cf6fa116c9" + ], + "sequence": 2147483648 + } + ], + "vout": [ + { + "value": 0.01003881, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 04b8d73fbfeccaea8c253279811e60c5e1d4a9a8 OP_EQUAL", + "hex": "a91404b8d73fbfeccaea8c253279811e60c5e1d4a9a887", + "type": "scripthash" + } + }, + { + "value": 0.01442799, + "n": 1, + "scriptPubKey": { + "asm": "0 811535e6ed76ba320d54bb8f418fcb53d527e2c4", + "hex": "0014811535e6ed76ba320d54bb8f418fcb53d527e2c4", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json new file mode 100644 index 0000000000..2de51e3356 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json @@ -0,0 +1,42 @@ +{ + "hex": "02000000000101ab6912b3e805bb584ed67d71aea4f5504f18c279d77409001b0fe307277e23020100000000ffffffff02c8af000000000000225120ac30d6fed6e38b53ea9e2f78b37366dd99c0df7408ab2627c9d05b7a3c2ae0816efd00000000000016001433d1c36b26902298ac55fc176653854fc2f9bc6e02473044022072fee83302148a971a5e1c24e063d518665dfe76c1791f20da5ec8df4a352fdb02204f1a2ba4068b86fe3c1ca2ec77a5c45f0dcb3af8d9dbc2d38091afae6011d83f0121038bdc9021a5d81cbfc28b5b41d2465ca891ba233e9d6ca72cfef654a1ef37749200000000", + "txid": "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7", + "hash": "253639775c305a4c28707be417bbb64ce3fa1b33ce582a60f95262f0e847d553", + "size": 234, + "vsize": 153, + "weight": 609, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "02237e2707e30f1b000974d779c2184f50f5a4ae717dd64e58bb05e8b31269ab", + "vout": 1, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022072fee83302148a971a5e1c24e063d518665dfe76c1791f20da5ec8df4a352fdb02204f1a2ba4068b86fe3c1ca2ec77a5c45f0dcb3af8d9dbc2d38091afae6011d83f01", + "038bdc9021a5d81cbfc28b5b41d2465ca891ba233e9d6ca72cfef654a1ef377492" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.00045, + "n": 0, + "scriptPubKey": { + "asm": "1 ac30d6fed6e38b53ea9e2f78b37366dd99c0df7408ab2627c9d05b7a3c2ae081", + "hex": "5120ac30d6fed6e38b53ea9e2f78b37366dd99c0df7408ab2627c9d05b7a3c2ae081", + "type": "witness_v1_taproot" + } + }, + { + "value": 0.00064878, + "n": 1, + "scriptPubKey": { + "asm": "0 33d1c36b26902298ac55fc176653854fc2f9bc6e", + "hex": "001433d1c36b26902298ac55fc176653854fc2f9bc6e", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json new file mode 100644 index 0000000000..4f68b6653c --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json @@ -0,0 +1,42 @@ +{ + "hex": "0100000000010147073577cf72e6227bb0bfbcc4ada7ecb28323c32c5c3b7c415b2b6e873022e37600000000fdffffff024238010000000000160014e99275308221c877b1ef7e7cbd55015e67e475d9bee10e0000000000225120ce403c2e1563b1e05eb213b4f601c2adf26293b86532146aab241c51a742eacf024730440220126e8c6a5fb11a1c0cbafc348601d427e9126d17c7136cb99fa6e6e61259066b02206fae29f384575cfed54946794b8784bf0f68b4ea5ed6caa5a12bb7d18a226de30121031f88031b5aa6e540ac109e4b6875b1c2826f84a3a4b23f4520d65ed54947b13b00000000", + "txid": "5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b", + "hash": "197248f8cae9b569f871f728b8163f1f322c145095b4df6425578e752928873e", + "size": 234, + "vsize": 153, + "weight": 609, + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "e32230876e2b5b417c3b5c2cc32383b2eca7adc4bcbfb07b22e672cf77350747", + "vout": 118, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "30440220126e8c6a5fb11a1c0cbafc348601d427e9126d17c7136cb99fa6e6e61259066b02206fae29f384575cfed54946794b8784bf0f68b4ea5ed6caa5a12bb7d18a226de301", + "031f88031b5aa6e540ac109e4b6875b1c2826f84a3a4b23f4520d65ed54947b13b" + ], + "sequence": 4294967293 + } + ], + "vout": [ + { + "value": 0.00079938, + "n": 0, + "scriptPubKey": { + "asm": "0 e99275308221c877b1ef7e7cbd55015e67e475d9", + "hex": "0014e99275308221c877b1ef7e7cbd55015e67e475d9", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0.00975294, + "n": 1, + "scriptPubKey": { + "asm": "1 ce403c2e1563b1e05eb213b4f601c2adf26293b86532146aab241c51a742eacf", + "hex": "5120ce403c2e1563b1e05eb213b4f601c2adf26293b86532146aab241c51a742eacf", + "type": "witness_v1_taproot" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json new file mode 100644 index 0000000000..adc78f14c8 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json @@ -0,0 +1,73 @@ +{ + "hex": "0200000000010583fd136d03a9483695a252222fb2d0cb73ba8c76793ad5f876c375f58aa300920600000000ffffffff5a0e44799e3256044c7f4a89ed2c0d1e1e4341c8d9d41b08b96d2b0592d1d7c00e00000000ffffffff48245fa209bbdcbe9009a5a38eb871972ae1dcf6ffe665095fe4f142682401690c00000000ffffffff6b288db05995de2a600225624e9e3e9f59eadb8f3a9654de144d953df62a12831300000000ffffffff5354734d187ee87102beea3c59b707337281e269e869657936ef271cb40a01b90000000000ffffffff0193d12d02000000002200200334174ebe7b38f5c20d4dfb5136c72b2383e2f4fc26421e94b16f81e191cb3202473044022100c532ad6675bdd13b3c8dd3202d6d576bfb9db739f2b8f3895e5358c075c1273f021f4b201148c88cc8208f32151490e3b236c473553ffd6fe3e7a4d14fe3291b1f012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02483045022100d374b7f988b8aeaf8a971ff6358373e3aa015e5ab9c8a325bfa837c36ffef6360220615ce57849e5499592161f404493b6db197021e280c34d0cf30fd825d3bbddae012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02473044022046cc9dc40d51a6978edcb15b6b404618691f3ff0255f1afa431a973d7246c8c3022027e64ec89d09862ef8485f54352728344ca871b4f5bc536f019db07023f8d3bf012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02473044022037dc56be5a40c2f7f13da618aa56745c8ef6f60ae70e926bf6af55b504179dd902200227ab4ba869dd658f7956161c0d3d5df2fa6255ea064839588d9a42636b15c6012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02473044022033c0b3daac006e07e7537e7f821ad60ae363a1c7cf01ae4dcd1cba1f1409cadd02200844428654b8ed4f6e6e3cab7d7613666eaf667f8d8b1e0e7c7ac921838137a3012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a00000000", + "txid": "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53", + "hash": "f9610c875ed2084ea51e8c87077f4eb2d6bfc9c40a6a4ab011046b59a1a35a04", + "size": 796, + "vsize": 393, + "weight": 1570, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "9200a38af575c376f8d53a79768cba73cbd0b22f2252a2953648a9036d13fd83", + "vout": 6, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022100c532ad6675bdd13b3c8dd3202d6d576bfb9db739f2b8f3895e5358c075c1273f021f4b201148c88cc8208f32151490e3b236c473553ffd6fe3e7a4d14fe3291b1f01", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "c0d7d192052b6db9081bd4d9c841431e1e0d2ced894a7f4c0456329e79440e5a", + "vout": 14, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3045022100d374b7f988b8aeaf8a971ff6358373e3aa015e5ab9c8a325bfa837c36ffef6360220615ce57849e5499592161f404493b6db197021e280c34d0cf30fd825d3bbddae01", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "6901246842f1e45f0965e6fff6dce12a9771b88ea3a50990bedcbb09a25f2448", + "vout": 12, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022046cc9dc40d51a6978edcb15b6b404618691f3ff0255f1afa431a973d7246c8c3022027e64ec89d09862ef8485f54352728344ca871b4f5bc536f019db07023f8d3bf01", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "83122af63d954d14de54963a8fdbea599f3e9e4e622502602ade9559b08d286b", + "vout": 19, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022037dc56be5a40c2f7f13da618aa56745c8ef6f60ae70e926bf6af55b504179dd902200227ab4ba869dd658f7956161c0d3d5df2fa6255ea064839588d9a42636b15c601", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "b9010ab41c27ef36796569e869e281723307b7593ceabe0271e87e184d735453", + "vout": 0, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022033c0b3daac006e07e7537e7f821ad60ae363a1c7cf01ae4dcd1cba1f1409cadd02200844428654b8ed4f6e6e3cab7d7613666eaf667f8d8b1e0e7c7ac921838137a301", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.36557203, + "n": 0, + "scriptPubKey": { + "asm": "0 0334174ebe7b38f5c20d4dfb5136c72b2383e2f4fc26421e94b16f81e191cb32", + "hex": "00200334174ebe7b38f5c20d4dfb5136c72b2383e2f4fc26421e94b16f81e191cb32", + "type": "witness_v0_scripthash" + } + } + ] +} diff --git a/zetaclient/testutils/stub/tss_signer.go b/zetaclient/testutils/stub/tss_signer.go index da3f954588..fa8f21747a 100644 --- a/zetaclient/testutils/stub/tss_signer.go +++ b/zetaclient/testutils/stub/tss_signer.go @@ -26,23 +26,25 @@ func init() { // TSS is a mock of TSS signer for testing type TSS struct { + chain common.Chain evmAddress string btcAddress string } -func NewMockTSS(evmAddress string, btcAddress string) *TSS { +func NewMockTSS(chain common.Chain, evmAddress string, btcAddress string) *TSS { return &TSS{ + chain: chain, evmAddress: evmAddress, btcAddress: btcAddress, } } func NewTSSMainnet() *TSS { - return NewMockTSS(testutils.TSSAddressEVMMainnet, testutils.TSSAddressBTCMainnet) + return NewMockTSS(common.BtcMainnetChain(), testutils.TSSAddressEVMMainnet, testutils.TSSAddressBTCMainnet) } func NewTSSAthens3() *TSS { - return NewMockTSS(testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) + return NewMockTSS(common.BscTestnetChain(), testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) } // Sign uses test key unrelated to any tss key in production @@ -76,7 +78,16 @@ func (s *TSS) BTCAddress() string { } func (s *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { - return nil + net, err := common.GetBTCChainParams(s.chain.ChainId) + if err != nil { + panic(err) + } + tssAddress := s.BTCAddress() + addr, err := btcutil.DecodeAddress(tssAddress, net) + if err != nil { + return nil + } + return addr.(*btcutil.AddressWitnessPubKeyHash) } func (s *TSS) PubKeyCompressedBytes() []byte { diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index 0676824514..11d12694f7 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -10,7 +10,6 @@ import ( "github.com/btcsuite/btcd/btcjson" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/onrik/ethrpc" - "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/common" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/config" @@ -38,16 +37,19 @@ func SaveObjectToJSONFile(obj interface{}, filename string) error { } // LoadObjectFromJSONFile loads an object from a file in JSON format -func LoadObjectFromJSONFile(obj interface{}, filename string) error { +func LoadObjectFromJSONFile(obj interface{}, filename string) { file, err := os.Open(filepath.Clean(filename)) if err != nil { - return err + panic(err) } defer file.Close() // read the struct from the file decoder := json.NewDecoder(file) - return decoder.Decode(&obj) + err = decoder.Decode(&obj) + if err != nil { + panic(err) + } } func ComplianceConfigTest() config.ComplianceConfig { @@ -78,81 +80,74 @@ func SaveBTCBlockTrimTx(blockVb *btcjson.GetBlockVerboseTxResult, filename strin } // LoadEVMBlock loads archived evm block from file -func LoadEVMBlock(t *testing.T, chainID int64, blockNumber uint64, trimmed bool) *ethrpc.Block { +func LoadEVMBlock(chainID int64, blockNumber uint64, trimmed bool) *ethrpc.Block { name := path.Join("../", TestDataPathEVM, FileNameEVMBlock(chainID, blockNumber, trimmed)) block := ðrpc.Block{} - err := LoadObjectFromJSONFile(block, name) - require.NoError(t, err) + LoadObjectFromJSONFile(block, name) return block } // LoadBTCTxRawResultNCctx loads archived Bitcoin outtx raw result and corresponding cctx -func LoadBTCTxRawResultNCctx(t *testing.T, chainID int64, nonce uint64) (*btcjson.TxRawResult, *crosschaintypes.CrossChainTx) { +func LoadBTCTxRawResultNCctx(chainID int64, nonce uint64) (*btcjson.TxRawResult, *crosschaintypes.CrossChainTx) { //nameTx := FileNameBTCOuttx(chainID, nonce) nameTx := path.Join("../", TestDataPathBTC, FileNameBTCOuttx(chainID, nonce)) rawResult := &btcjson.TxRawResult{} - err := LoadObjectFromJSONFile(rawResult, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(rawResult, nameTx) nameCctx := path.Join("../", TestDataPathCctx, FileNameCctxByNonce(chainID, nonce)) cctx := &crosschaintypes.CrossChainTx{} - err = LoadObjectFromJSONFile(cctx, nameCctx) - require.NoError(t, err) + LoadObjectFromJSONFile(cctx, nameCctx) return rawResult, cctx } // LoadEVMIntx loads archived intx from file func LoadEVMIntx( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethrpc.Transaction { nameTx := path.Join("../", TestDataPathEVM, FileNameEVMIntx(chainID, intxHash, coinType, false)) tx := ðrpc.Transaction{} - err := LoadObjectFromJSONFile(&tx, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(&tx, nameTx) return tx } // LoadEVMIntxReceipt loads archived intx receipt from file func LoadEVMIntxReceipt( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Receipt { nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMIntxReceipt(chainID, intxHash, coinType, false)) receipt := ðtypes.Receipt{} - err := LoadObjectFromJSONFile(&receipt, nameReceipt) - require.NoError(t, err) + LoadObjectFromJSONFile(&receipt, nameReceipt) return receipt } // LoadEVMIntxCctx loads archived intx cctx from file func LoadEVMIntxCctx( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *crosschaintypes.CrossChainTx { nameCctx := path.Join("../", TestDataPathCctx, FileNameEVMIntxCctx(chainID, intxHash, coinType)) cctx := &crosschaintypes.CrossChainTx{} - err := LoadObjectFromJSONFile(&cctx, nameCctx) - require.NoError(t, err) + LoadObjectFromJSONFile(&cctx, nameCctx) return cctx } // LoadCctxByNonce loads archived cctx by nonce from file func LoadCctxByNonce( - t *testing.T, + _ *testing.T, chainID int64, nonce uint64) *crosschaintypes.CrossChainTx { nameCctx := path.Join("../", TestDataPathCctx, FileNameCctxByNonce(chainID, nonce)) cctx := &crosschaintypes.CrossChainTx{} - err := LoadObjectFromJSONFile(&cctx, nameCctx) - require.NoError(t, err) + LoadObjectFromJSONFile(&cctx, nameCctx) return cctx } @@ -171,29 +166,27 @@ func LoadEVMIntxNReceipt( // LoadEVMIntxDonation loads archived donation intx from file func LoadEVMIntxDonation( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethrpc.Transaction { nameTx := path.Join("../", TestDataPathEVM, FileNameEVMIntx(chainID, intxHash, coinType, true)) tx := ðrpc.Transaction{} - err := LoadObjectFromJSONFile(&tx, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(&tx, nameTx) return tx } // LoadEVMIntxReceiptDonation loads archived donation intx receipt from file func LoadEVMIntxReceiptDonation( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Receipt { nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMIntxReceipt(chainID, intxHash, coinType, true)) receipt := ðtypes.Receipt{} - err := LoadObjectFromJSONFile(&receipt, nameReceipt) - require.NoError(t, err) + LoadObjectFromJSONFile(&receipt, nameReceipt) return receipt } @@ -226,29 +219,27 @@ func LoadEVMIntxNReceiptNCctx( // LoadEVMOuttx loads archived evm outtx from file func LoadEVMOuttx( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Transaction { nameTx := path.Join("../", TestDataPathEVM, FileNameEVMOuttx(chainID, intxHash, coinType)) tx := ðtypes.Transaction{} - err := LoadObjectFromJSONFile(&tx, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(&tx, nameTx) return tx } // LoadEVMOuttxReceipt loads archived evm outtx receipt from file func LoadEVMOuttxReceipt( - t *testing.T, + _ *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Receipt { nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMOuttxReceipt(chainID, intxHash, coinType)) receipt := ðtypes.Receipt{} - err := LoadObjectFromJSONFile(&receipt, nameReceipt) - require.NoError(t, err) + LoadObjectFromJSONFile(&receipt, nameReceipt) return receipt } diff --git a/zetaclient/testutils/testdata_naming.go b/zetaclient/testutils/testdata_naming.go index 404b2471eb..b417435237 100644 --- a/zetaclient/testutils/testdata_naming.go +++ b/zetaclient/testutils/testdata_naming.go @@ -48,6 +48,12 @@ func FileNameBTCOuttx(chainID int64, nonce uint64) string { return fmt.Sprintf("chain_%d_outtx_raw_result_nonce_%d.json", chainID, nonce) } +// FileNameBTCTxByType returns unified archive file name for tx by type +// txType: "P2TR", "P2WPKH", "P2WSH", "P2PKH", "P2SH +func FileNameBTCTxByType(chainID int64, txType string, txHash string) string { + return fmt.Sprintf("chain_%d_tx_raw_result_%s_%s.json", chainID, txType, txHash) +} + // FileNameCctxByNonce returns unified archive file name for cctx by nonce func FileNameCctxByNonce(chainID int64, nonce uint64) string { return fmt.Sprintf("cctx_%d_%d.json", chainID, nonce)