From 18c7196c83c7a4df21142a197d878b1968d0b844 Mon Sep 17 00:00:00 2001 From: brewmaster012 <88689859+brewmaster012@users.noreply.github.com> Date: Wed, 6 Mar 2024 00:00:57 -0600 Subject: [PATCH 01/16] backport/add a AddressTaproot type that satisfies btcutil.Address interface --- common/bitcoin/address_taproot.go | 210 +++++++++++++++++++++++++ common/bitcoin/address_taproot_test.go | 59 +++++++ 2 files changed, 269 insertions(+) create mode 100644 common/bitcoin/address_taproot.go create mode 100644 common/bitcoin/address_taproot_test.go diff --git a/common/bitcoin/address_taproot.go b/common/bitcoin/address_taproot.go new file mode 100644 index 0000000000..f349c843dd --- /dev/null +++ b/common/bitcoin/address_taproot.go @@ -0,0 +1,210 @@ +package bitcoin + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/btcsuite/btcd/btcutil/bech32" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" +) + +// taproot address type + +type AddressSegWit struct { + hrp string + witnessVersion byte + witnessProgram []byte +} + +type AddressTaproot struct { + AddressSegWit +} + +var _ btcutil.Address = AddressTaproot{} + +// NewAddressTaproot returns a new AddressTaproot. +func NewAddressTaproot(witnessProg []byte, + net *chaincfg.Params) (*AddressTaproot, error) { + + return newAddressTaproot(net.Bech32HRPSegwit, witnessProg) +} + +// newAddressWitnessScriptHash is an internal helper function to create an +// AddressWitnessScriptHash with a known human-readable part, rather than +// looking it up through its parameters. +func newAddressTaproot(hrp string, witnessProg []byte) (*AddressTaproot, error) { + // Check for valid program length for witness version 1, which is 32 + // for P2TR. + if len(witnessProg) != 32 { + return nil, errors.New("witness program must be 32 bytes for " + + "p2tr") + } + + addr := &AddressTaproot{ + AddressSegWit{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x01, + witnessProgram: witnessProg, + }, + } + + return addr, nil +} + +// EncodeAddress returns the bech32 (or bech32m for SegWit v1) string encoding +// of an AddressSegWit. +// +// NOTE: This method is part of the Address interface. +func (a AddressSegWit) EncodeAddress() string { + str, err := encodeSegWitAddress( + a.hrp, a.witnessVersion, a.witnessProgram[:], + ) + if err != nil { + return "" + } + return str +} + +// encodeSegWitAddress creates a bech32 (or bech32m for SegWit v1) encoded +// address string representation from witness version and witness program. +func encodeSegWitAddress(hrp string, witnessVersion byte, witnessProgram []byte) (string, error) { + // Group the address bytes into 5 bit groups, as this is what is used to + // encode each character in the address string. + converted, err := bech32.ConvertBits(witnessProgram, 8, 5, true) + if err != nil { + return "", err + } + + // Concatenate the witness version and program, and encode the resulting + // bytes using bech32 encoding. + combined := make([]byte, len(converted)+1) + combined[0] = witnessVersion + copy(combined[1:], converted) + + var bech string + switch witnessVersion { + case 0: + bech, err = bech32.Encode(hrp, combined) + + case 1: + bech, err = bech32.EncodeM(hrp, combined) + + default: + return "", fmt.Errorf("unsupported witness version %d", + witnessVersion) + } + if err != nil { + return "", err + } + + // Check validity by decoding the created address. + _, version, program, err := decodeSegWitAddress(bech) + if err != nil { + return "", fmt.Errorf("invalid segwit address: %v", err) + } + + if version != witnessVersion || !bytes.Equal(program, witnessProgram) { + return "", fmt.Errorf("invalid segwit address") + } + + return bech, nil +} + +// decodeSegWitAddress parses a bech32 encoded segwit address string and +// returns the witness version and witness program byte representation. +func decodeSegWitAddress(address string) (string, byte, []byte, error) { + // Decode the bech32 encoded address. + hrp, data, bech32version, err := bech32.DecodeGeneric(address) + if err != nil { + return "", 0, nil, err + } + + // The first byte of the decoded address is the witness version, it must + // exist. + if len(data) < 1 { + return "", 0, nil, fmt.Errorf("no witness version") + } + + // ...and be <= 16. + version := data[0] + if version > 16 { + return "", 0, nil, fmt.Errorf("invalid witness version: %v", version) + } + + // The remaining characters of the address returned are grouped into + // words of 5 bits. In order to restore the original witness program + // bytes, we'll need to regroup into 8 bit words. + regrouped, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return "", 0, nil, err + } + + // The regrouped data must be between 2 and 40 bytes. + if len(regrouped) < 2 || len(regrouped) > 40 { + return "", 0, nil, fmt.Errorf("invalid data length") + } + + // For witness version 0, address MUST be exactly 20 or 32 bytes. + if version == 0 && len(regrouped) != 20 && len(regrouped) != 32 { + return "", 0, nil, fmt.Errorf("invalid data length for witness "+ + "version 0: %v", len(regrouped)) + } + + // For witness version 0, the bech32 encoding must be used. + if version == 0 && bech32version != bech32.Version0 { + return "", 0, nil, fmt.Errorf("invalid checksum expected bech32 " + + "encoding for address with witness version 0") + } + + // For witness version 1, the bech32m encoding must be used. + if version == 1 && bech32version != bech32.VersionM { + return "", 0, nil, fmt.Errorf("invalid checksum expected bech32m " + + "encoding for address with witness version 1") + } + + return hrp, version, regrouped, nil +} + +// ScriptAddress returns the witness program for this address. +// +// NOTE: This method is part of the Address interface. +func (a AddressSegWit) ScriptAddress() []byte { + return a.witnessProgram[:] +} + +// IsForNet returns whether the AddressSegWit is associated with the passed +// bitcoin network. +// +// NOTE: This method is part of the Address interface. +func (a AddressSegWit) IsForNet(net *chaincfg.Params) bool { + return a.hrp == net.Bech32HRPSegwit +} + +// String returns a human-readable string for the AddressWitnessPubKeyHash. +// This is equivalent to calling EncodeAddress, but is provided so the type +// can be used as a fmt.Stringer. +// +// NOTE: This method is part of the Address interface. +func (a AddressSegWit) String() string { + return a.EncodeAddress() +} + +func DecodeTaprootAddress(addr string) (AddressTaproot, error) { + hrp, version, program, err := decodeSegWitAddress(addr) + if err != nil { + return AddressTaproot{}, err + } + if version != 1 { + return AddressTaproot{}, errors.New("invalid witness version; taproot address must be version 1") + } + return AddressTaproot{ + AddressSegWit{ + hrp: hrp, + witnessVersion: version, + witnessProgram: program, + }, + }, nil +} diff --git a/common/bitcoin/address_taproot_test.go b/common/bitcoin/address_taproot_test.go new file mode 100644 index 0000000000..ce07eeacb0 --- /dev/null +++ b/common/bitcoin/address_taproot_test.go @@ -0,0 +1,59 @@ +package bitcoin + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" +) + +func TestAddressTaproot(t *testing.T) { + { + // should parse mainnet taproot address + addrStr := "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + t.Log(addr.String()) + require.Equal(t, addrStr, addr.String()) + require.Equal(t, addrStr, addr.EncodeAddress()) + require.True(t, addr.IsForNet(&chaincfg.MainNetParams)) + } + { + // should parse testnet taproot address + addrStr := "tb1pzeclkt6upu8xwuksjcz36y4q56dd6jw5r543eu8j8238yaxpvcvq7t8f33" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + t.Log(addr.String()) + require.Equal(t, addrStr, addr.String()) + require.Equal(t, addrStr, addr.EncodeAddress()) + require.True(t, addr.IsForNet(&chaincfg.TestNet3Params)) + } + { + // should parse regtest taproot address + addrStr := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + t.Log(addr.String()) + require.Equal(t, addrStr, addr.String()) + require.Equal(t, addrStr, addr.EncodeAddress()) + require.True(t, addr.IsForNet(&chaincfg.RegressionNetParams)) + } + + { + // should fail to parse invalid taproot address + // should parse mainnet taproot address + addrStr := "bc1qysd4sp9q8my59ul9wsf5rvs9p387hf8vfwatzu" + _, err := DecodeTaprootAddress(addrStr) + require.Error(t, err) + } + { + var witnessProg [32]byte + for i := 0; i < 32; i++ { + witnessProg[i] = byte(i) + } + _, err := newAddressTaproot("bcrt", witnessProg[:]) + require.Nil(t, err) + //t.Logf("addr: %v", addr) + } + +} From f78eb47767244efaf53746302c84cf0d3d8a62f4 Mon Sep 17 00:00:00 2001 From: brewmaster012 <88689859+brewmaster012@users.noreply.github.com> Date: Wed, 6 Mar 2024 00:20:40 -0600 Subject: [PATCH 02/16] support taproot in DecodeBtcAddress added more unit tests --- common/address.go | 7 +++++++ common/address_test.go | 9 +++++++++ common/bitcoin/address_taproot_test.go | 22 +++++++++++++++++++--- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/common/address.go b/common/address.go index c79a4c5c9f..a571f58d45 100644 --- a/common/address.go +++ b/common/address.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcutil" eth "github.com/ethereum/go-ethereum/common" + "github.com/zeta-chain/zetacore/common/bitcoin" "github.com/zeta-chain/zetacore/common/cosmos" ) @@ -71,6 +72,12 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre if chainParams == nil { return nil, fmt.Errorf("chain params not found") } + // test taproot address type + address, err = bitcoin.DecodeTaprootAddress(inputAddress) + if err == nil && address.IsForNet(chainParams) { + return address, nil + } + // test taproot address fail; continue other types address, err = btcutil.DecodeAddress(inputAddress, chainParams) if err != nil { return nil, fmt.Errorf("decode address failed: %s , for input address %s", err.Error(), inputAddress) diff --git a/common/address_test.go b/common/address_test.go index d39945935c..51699f25d0 100644 --- a/common/address_test.go +++ b/common/address_test.go @@ -60,4 +60,13 @@ func TestDecodeBtcAddress(t *testing.T) { _, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcRegtestChain().ChainId) require.NoError(t, err) }) + + t.Run("taproot address with correct params", func(t *testing.T) { + _, err := DecodeBtcAddress("bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c", BtcMainnetChain().ChainId) + require.NoError(t, err) + }) + t.Run("taproot address with incorrect params", func(t *testing.T) { + _, err := DecodeBtcAddress("bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c", BtcTestNetChain().ChainId) + require.Error(t, err) + }) } diff --git a/common/bitcoin/address_taproot_test.go b/common/bitcoin/address_taproot_test.go index ce07eeacb0..c7af37e75a 100644 --- a/common/bitcoin/address_taproot_test.go +++ b/common/bitcoin/address_taproot_test.go @@ -1,6 +1,7 @@ package bitcoin import ( + "encoding/hex" "testing" "github.com/btcsuite/btcd/chaincfg" @@ -13,7 +14,6 @@ func TestAddressTaproot(t *testing.T) { addrStr := "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c" addr, err := DecodeTaprootAddress(addrStr) require.Nil(t, err) - t.Log(addr.String()) require.Equal(t, addrStr, addr.String()) require.Equal(t, addrStr, addr.EncodeAddress()) require.True(t, addr.IsForNet(&chaincfg.MainNetParams)) @@ -23,7 +23,6 @@ func TestAddressTaproot(t *testing.T) { addrStr := "tb1pzeclkt6upu8xwuksjcz36y4q56dd6jw5r543eu8j8238yaxpvcvq7t8f33" addr, err := DecodeTaprootAddress(addrStr) require.Nil(t, err) - t.Log(addr.String()) require.Equal(t, addrStr, addr.String()) require.Equal(t, addrStr, addr.EncodeAddress()) require.True(t, addr.IsForNet(&chaincfg.TestNet3Params)) @@ -33,7 +32,6 @@ func TestAddressTaproot(t *testing.T) { addrStr := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" addr, err := DecodeTaprootAddress(addrStr) require.Nil(t, err) - t.Log(addr.String()) require.Equal(t, addrStr, addr.String()) require.Equal(t, addrStr, addr.EncodeAddress()) require.True(t, addr.IsForNet(&chaincfg.RegressionNetParams)) @@ -55,5 +53,23 @@ func TestAddressTaproot(t *testing.T) { require.Nil(t, err) //t.Logf("addr: %v", addr) } + { + // should create correct taproot address from given witness program + // these hex string comes from link + // https://mempool.space/tx/41f7cbaaf9a8d378d09ee86de32eebef455225520cb71015cc9a7318fb42e326 + witnessProg, err := hex.DecodeString("af06f3d4c726a3b952f2f648d86398af5ddfc3df27aa531d97203987add8db2e") + addr, err := newAddressTaproot("bc", witnessProg[:]) + require.Nil(t, err) + require.Equal(t, addr.EncodeAddress(), "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c") + } + { + // should give correct ScriptAddress for taproot address + // example comes from + // https://blockstream.info/tx/09298a2f32f5267f419aeaf8a58c4807dcf6cac3edb59815a3b129cd8f1219b0?expand + addrStr := "bc1p6pls9gpm24g8ntl37pajpjtuhd3y08hs5rnf9a4n0wq595hwdh9suw7m2h" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + require.Equal(t, "d07f02a03b555079aff1f07b20c97cbb62479ef0a0e692f6b37b8142d2ee6dcb", hex.EncodeToString(addr.ScriptAddress())) + } } From 0c167cdd1777e7d7dd76dd194e2b47ef89be76d2 Mon Sep 17 00:00:00 2001 From: brewmaster012 <88689859+brewmaster012@users.noreply.github.com> Date: Wed, 6 Mar 2024 00:22:08 -0600 Subject: [PATCH 03/16] minor tweak --- common/bitcoin/address_taproot_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/bitcoin/address_taproot_test.go b/common/bitcoin/address_taproot_test.go index c7af37e75a..197fa99db2 100644 --- a/common/bitcoin/address_taproot_test.go +++ b/common/bitcoin/address_taproot_test.go @@ -58,7 +58,7 @@ func TestAddressTaproot(t *testing.T) { // these hex string comes from link // https://mempool.space/tx/41f7cbaaf9a8d378d09ee86de32eebef455225520cb71015cc9a7318fb42e326 witnessProg, err := hex.DecodeString("af06f3d4c726a3b952f2f648d86398af5ddfc3df27aa531d97203987add8db2e") - addr, err := newAddressTaproot("bc", witnessProg[:]) + addr, err := NewAddressTaproot(witnessProg[:], &chaincfg.MainNetParams) require.Nil(t, err) require.Equal(t, addr.EncodeAddress(), "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c") } From c2dfb7a615e1f1972631c0eea5a0c057d7a1bb4b Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 22 Mar 2024 00:11:36 -0500 Subject: [PATCH 04/16] 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) From 738d2659752ef63bed705bbcb7c84832f762f4b6 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 22 Mar 2024 10:50:21 -0500 Subject: [PATCH 05/16] added decoding for more intx types --- zetaclient/bitcoin/bitcoin_client.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 9cf1a81c52..532be9586b 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -721,9 +721,9 @@ func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { return false } -// GetSenderAddressByVinP2TR get the P2TR sender address from the previous transaction +// GetSenderAddressByVin get the 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) { +func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { // query previous transaction hash, res, err := GetTxResultByHash(rpcClient, vin.Txid) if err != nil { @@ -738,10 +738,19 @@ func GetSenderAddressByVinP2TR(rpcClient interfaces.BTCRPCClient, vin btcjson.Vi 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 + // decode sender address from previous vout script vout := rawResult.Vout[vin.Vout] - if vout.ScriptPubKey.Type == ScriptTypeP2TR { + switch vout.ScriptPubKey.Type { + case ScriptTypeP2TR: return DecodeVoutP2TR(vout, net) + case ScriptTypeP2WSH: + return DecodeVoutP2WSH(vout, net) + case ScriptTypeP2WPKH: + return DecodeVoutP2WPKH(vout, net) + case ScriptTypeP2SH: + return DecodeVoutP2SH(vout, net) + case ScriptTypeP2PKH: + return DecodeVoutP2PKH(vout, net) } return "", nil } @@ -799,8 +808,8 @@ 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) + } else { // try getting sender address from previous output + sender, err := GetSenderAddressByVin(rpcClient, vin, netParams) if err != nil { return nil, errors.Wrapf(err, "error getting sender address") } From 291322b47582886d791c3e3f8c128f20bfc444a6 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Sun, 24 Mar 2024 12:57:00 -0500 Subject: [PATCH 06/16] added btc intx address decoding and tests --- e2e/runner/bitcoin.go | 5 +- zetaclient/bitcoin/bitcoin_client.go | 160 ++++---- zetaclient/bitcoin/bitcoin_client_rpc_test.go | 8 +- zetaclient/bitcoin/bitcoin_client_test.go | 357 ++++++++++++++++++ zetaclient/bitcoin/txscript.go | 207 +++++----- zetaclient/bitcoin/txscript_test.go | 211 +++++++---- zetaclient/interfaces/interfaces.go | 1 + ...7ad049d49719c3493e6495c88f969bae1c570.json | 50 --- ...1a18b9e853dbdf265ebb1c728f9b52813455a.json | 25 ++ ...aa8b767b4a5d6d60b143c2c50af52b257e867.json | 29 ++ ...573fda52c8fdbba5e78152aeb4432286836a7.json | 1 + ...42a17b25e481a88cc9119008e8f8296652697.json | 42 +++ ...a0b0f318feaea283185c1cddb8b341c27c016.json | 28 ++ zetaclient/testutils/stub/btc_rpc.go | 121 ++++++ zetaclient/testutils/testdata.go | 8 + zetaclient/testutils/testdata_naming.go | 5 + 16 files changed, 929 insertions(+), 329 deletions(-) delete mode 100644 zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json create mode 100644 zetaclient/testdata/btc/chain_8332_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json create mode 100644 zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json create mode 100644 zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json create mode 100644 zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json create mode 100644 zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json create mode 100644 zetaclient/testutils/stub/btc_rpc.go diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index e4af60c701..80234c1265 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -265,7 +265,7 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( } depositorFee := zetabitcoin.DefaultDepositorFee - events := zetabitcoin.FilterAndParseIncomingTx( + events, err := zetabitcoin.FilterAndParseIncomingTx( btcRPC, []btcjson.TxRawResult{*rawtx}, 0, @@ -274,6 +274,9 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( runner.BitcoinParams, depositorFee, ) + if err != nil { + panic(err) + } runner.Logger.Info("bitcoin intx events:") for _, event := range events { runner.Logger.Info(" TxHash: %s", event.TxHash) diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 532be9586b..4f5b5371ba 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -446,7 +446,7 @@ func (ob *BTCChainClient) observeInTx() error { // filter incoming txs to TSS address tssAddress := ob.Tss.BTCAddress() // #nosec G701 always positive - inTxs := FilterAndParseIncomingTx( + inTxs, err := FilterAndParseIncomingTx( ob.rpcClient, res.Block.Tx, uint64(res.Block.Height), @@ -455,6 +455,10 @@ func (ob *BTCChainClient) observeInTx() error { ob.netParams, depositorFee, ) + if err != nil { + ob.logger.WatchInTx.Error().Err(err).Msgf("observeInTxBTC: error filtering incoming txs for block %d", bn) + return err // we have to re-scan this block next time + } // post inbound vote message to zetacore for _, inTx := range inTxs { @@ -657,7 +661,7 @@ func FilterAndParseIncomingTx( logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, -) []*BTCInTxEvnet { +) ([]*BTCInTxEvnet, error) { inTxs := make([]*BTCInTxEvnet, 0) for idx, tx := range txs { if idx == 0 { @@ -665,15 +669,15 @@ func FilterAndParseIncomingTx( } 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 + // unable to parse the tx, the caller should retry + return nil, errors.Wrapf(err, "error getting btc event for tx %s in block %d", tx.Txid, blockNumber) } if inTx != nil { inTxs = append(inTxs, inTx) logger.Info().Msgf("FilterAndParseIncomingTx: found btc event for tx %s in block %d", tx.Txid, blockNumber) } } - return inTxs + return inTxs, nil } func (ob *BTCChainClient) GetInboundVoteMessageFromBtcEvent(inTx *BTCInTxEvnet) *types.MsgVoteOnObservedInboundTx { @@ -721,40 +725,8 @@ func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { return false } -// GetSenderAddressByVin get the sender address from the previous transaction -// Note: this function requires the bitcoin node index the previous transaction -func GetSenderAddressByVin(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 - vout := rawResult.Vout[vin.Vout] - switch vout.ScriptPubKey.Type { - case ScriptTypeP2TR: - return DecodeVoutP2TR(vout, net) - case ScriptTypeP2WSH: - return DecodeVoutP2WSH(vout, net) - case ScriptTypeP2WPKH: - return DecodeVoutP2WPKH(vout, net) - case ScriptTypeP2SH: - return DecodeVoutP2SH(vout, net) - case ScriptTypeP2PKH: - return DecodeVoutP2PKH(vout, net) - } - return "", nil -} - +// GetBtcEvent either returns a valid BTCInTxEvnet or nil +// Note: the caller should retry the tx on error (e.g., GetSenderAddressByVin failed) func GetBtcEvent( rpcClient interfaces.BTCRPCClient, tx btcjson.TxRawResult, @@ -769,52 +741,41 @@ func GetBtcEvent( var memo []byte if len(tx.Vout) >= 2 { // 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 { + vout0 := tx.Vout[0] + script := vout0.ScriptPubKey.Hex + if len(script) == 44 && script[:4] == "0014" { // P2WPKH output: 0x00 + 20 bytes of pubkey hash + receiver, err := DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) + if err != nil { // should never happen + return nil, err + } // 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 + if receiver != tssAddress { + return nil, nil + } + // deposit amount has to be no less than the minimum depositor fee + if vout0.Value < depositorFee { + logger.Info().Msgf("GetBtcEvent: btc deposit amount %v in txid %s is less than depositor fee %v", vout0.Value, tx.Txid, depositorFee) + return nil, nil + } + value = vout0.Value - depositorFee - // 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 + // 2nd vout must be a valid OP_RETURN memo + vout1 := tx.Vout[1] + memo, found, err = DecodeOpReturnMemo(vout1.ScriptPubKey.Hex, tx.Txid) + if err != nil { + logger.Error().Err(err).Msgf("GetBtcEvent: error decoding OP_RETURN memo: %s", vout1.ScriptPubKey.Hex) + return nil, nil + } } } + // event found, get sender address if found { - logger.Info().Msgf("found bitcoin intx: %s", tx.Txid) - var fromAddress string - if len(tx.Vin) > 0 { - vin := tx.Vin[0] - if len(vin.Witness) == 2 { // decode P2WPKH sender address - pk := vin.Witness[1] - pkBytes, err := hex.DecodeString(pk) - if err != nil { - return nil, errors.Wrapf(err, "error decoding pubkey") - } - hash := btcutil.Hash160(pkBytes) - addr, err := btcutil.NewAddressWitnessPubKeyHash(hash, netParams) - if err != nil { - return nil, errors.Wrapf(err, "error decoding pubkey hash") - } - fromAddress = addr.EncodeAddress() - } else { // try getting sender address from previous output - sender, err := GetSenderAddressByVin(rpcClient, vin, netParams) - if err != nil { - return nil, errors.Wrapf(err, "error getting sender address") - } - fromAddress = sender - } + if len(tx.Vin) == 0 { // should never happen + return nil, fmt.Errorf("GetBtcEvent: no input found for intx: %s", tx.Txid) + } + fromAddress, err := GetSenderAddressByVin(rpcClient, tx.Vin[0], netParams) + if err != nil { + return nil, errors.Wrapf(err, "error getting sender address for intx: %s", tx.Txid) } return &BTCInTxEvnet{ FromAddress: fromAddress, @@ -828,6 +789,45 @@ func GetBtcEvent( return nil, nil } +// GetSenderAddressByVin get the sender address from the previous transaction +func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { + // query previous raw transaction by txid + // GetTransaction requires reconfiguring the bitcoin node (txindex=1), so we use GetRawTransaction instead + hash, err := chainhash.NewHashFromStr(vin.Txid) + if err != nil { + return "", err + } + tx, err := rpcClient.GetRawTransaction(hash) + if err != nil { + return "", errors.Wrapf(err, "error getting raw transaction %s", vin.Txid) + } + // #nosec G701 - always in range + if len(tx.MsgTx().TxOut) <= int(vin.Vout) { + return "", fmt.Errorf("vout index %d out of range for tx %s", vin.Vout, vin.Txid) + } + + // decode sender address from previous pkScript + pkScript := tx.MsgTx().TxOut[vin.Vout].PkScript + scriptHex := hex.EncodeToString(pkScript) + if IsPkScriptP2TR(pkScript) { + return DecodeScriptP2TR(scriptHex, net) + } + if IsPkScriptP2WSH(pkScript) { + return DecodeScriptP2WSH(scriptHex, net) + } + if IsPkScriptP2WPKH(pkScript) { + return DecodeScriptP2WPKH(scriptHex, net) + } + if IsPkScriptP2SH(pkScript) { + return DecodeScriptP2SH(scriptHex, net) + } + if IsPkScriptP2PKH(pkScript) { + return DecodeScriptP2PKH(scriptHex, net) + } + // sender address not found, return nil and move on to the next tx + return "", nil +} + func (ob *BTCChainClient) WatchUTXOS() { ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchUTXOS", ob.GetChainParams().WatchUtxoTicker) if err != nil { diff --git a/zetaclient/bitcoin/bitcoin_client_rpc_test.go b/zetaclient/bitcoin/bitcoin_client_rpc_test.go index 6f0085e460..59d5f2e463 100644 --- a/zetaclient/bitcoin/bitcoin_client_rpc_test.go +++ b/zetaclient/bitcoin/bitcoin_client_rpc_test.go @@ -145,7 +145,7 @@ func (suite *BitcoinClientTestSuite) Test1() { suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) - inTxs := FilterAndParseIncomingTx( + inTxs, err := FilterAndParseIncomingTx( suite.BitcoinChainClient.rpcClient, block.Tx, uint64(block.Height), @@ -154,7 +154,7 @@ func (suite *BitcoinClientTestSuite) Test1() { &chaincfg.TestNet3Params, 0.0, ) - + suite.Require().NoError(err) suite.Require().Equal(1, len(inTxs)) suite.Require().Equal(inTxs[0].Value, 0.0001) suite.Require().Equal(inTxs[0].ToAddress, "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2") @@ -183,7 +183,7 @@ func (suite *BitcoinClientTestSuite) Test2() { suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) - inTxs := FilterAndParseIncomingTx( + inTxs, err := FilterAndParseIncomingTx( suite.BitcoinChainClient.rpcClient, block.Tx, uint64(block.Height), @@ -192,7 +192,7 @@ func (suite *BitcoinClientTestSuite) Test2() { &chaincfg.TestNet3Params, 0.0, ) - + suite.Require().NoError(err) suite.Require().Equal(0, len(inTxs)) } diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index f3e731c1dd..732f1c3a52 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -6,12 +6,14 @@ import ( "math" "math/big" "path" + "strings" "sync" "testing" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" @@ -327,3 +329,358 @@ func TestCheckTSSVoutCancelled(t *testing.T) { require.ErrorContains(t, err, "not match TSS address") }) } + +// createRPCClientAndLoadTx is a helper function to load raw tx and feed it to mock rpc client +func createRPCClientAndLoadTx(chainId int64, txHash string) *stub.MockBTCRPCClient { + // file name for the archived MsgTx + nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainId, txHash)) + + // load archived MsgTx + var msgTx wire.MsgTx + testutils.LoadObjectFromJSONFile(&msgTx, nameMsgTx) + tx := btcutil.NewTx(&msgTx) + + // feed tx to mock rpc client + rpcClient := stub.NewMockBTCRPCClient() + rpcClient.WithRawTransaction(tx) + return rpcClient +} + +func TestGetSenderAddressByVin(t *testing.T) { + chain := common.BtcMainnetChain() + net := &chaincfg.MainNetParams + + t.Run("should get sender address from P2TR tx", func(t *testing.T) { + // vin from the archived P2TR tx + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" + rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) + }) + t.Run("should get sender address from P2WSH tx", func(t *testing.T) { + // vin from the archived P2WSH tx + // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 + txHash := "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016" + rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 0} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) + }) + t.Run("should get sender address from P2WPKH tx", func(t *testing.T) { + // vin from the archived P2WPKH tx + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + txHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) + }) + t.Run("should get sender address from P2SH tx", func(t *testing.T) { + // vin from the archived P2SH tx + // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a + txHash := "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a" + rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 0} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) + }) + t.Run("should get sender address from P2PKH tx", func(t *testing.T) { + // vin from the archived P2PKH tx + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" + rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 1} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) + }) + t.Run("should get empty sender address on unknown script", func(t *testing.T) { + // vin from the archived P2PKH tx + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" + nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chain.ChainId, txHash)) + var msgTx wire.MsgTx + testutils.LoadObjectFromJSONFile(&msgTx, nameMsgTx) + + // modify script to unknown script + msgTx.TxOut[1].PkScript = []byte{0x00, 0x01, 0x02, 0x03} // can be any invalid script bytes + tx := btcutil.NewTx(&msgTx) + + // feed tx to mock rpc client + rpcClient := stub.NewMockBTCRPCClient() + rpcClient.WithRawTransaction(tx) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 1} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Empty(t, sender) + }) +} + +func TestGetSenderAddressByVinErrors(t *testing.T) { + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" + chain := common.BtcMainnetChain() + net := &chaincfg.MainNetParams + + t.Run("should get sender address from P2TR tx", func(t *testing.T) { + rpcClient := stub.NewMockBTCRPCClient() + // use invalid tx hash + txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.Error(t, err) + require.Empty(t, sender) + }) + t.Run("should return error when RPC client fails to get raw tx", func(t *testing.T) { + // create mock rpc client without preloaded tx + rpcClient := stub.NewMockBTCRPCClient() + txVin := btcjson.Vin{Txid: txHash, Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.ErrorContains(t, err, "error getting raw transaction") + require.Empty(t, sender) + }) + t.Run("should return error on invalid output index", func(t *testing.T) { + // create mock rpc client with preloaded tx + rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + // invalid output index + txVin := btcjson.Vin{Txid: txHash, Vout: 3} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.ErrorContains(t, err, "out of range") + require.Empty(t, sender) + }) +} + +func TestGetBtcEvent(t *testing.T) { + // load archived intx P2WPKH raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + chain := common.BtcMainnetChain() + + // GetBtcEvent arguments + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tssAddress := testutils.TSSAddressBTCMainnet + blockNumber := uint64(835640) + net := &chaincfg.MainNetParams + // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 + depositorFee := DepositorFee(22 * clientcommon.BTCOuttxGasPriceMultiplier) + + // expected result + memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) + require.NoError(t, err) + eventExpected := &BTCInTxEvnet{ + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, + } + + t.Run("should get BTC intx event from P2WPKH sender", func(t *testing.T) { + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 2 + eventExpected.FromAddress = "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2TR sender", func(t *testing.T) { + // replace vin with a P2TR vin, so the sender address will change + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + preHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 2 + eventExpected.FromAddress = "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2WSH sender", func(t *testing.T) { + // replace vin with a P2WSH vin, so the sender address will change + // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 + preHash := "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 0 + eventExpected.FromAddress = "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2SH sender", func(t *testing.T) { + // replace vin with a P2SH vin, so the sender address will change + // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a + preHash := "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 0 + eventExpected.FromAddress = "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2PKH sender", func(t *testing.T) { + // replace vin with a P2PKH vin, so the sender address will change + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + preHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 1 + eventExpected.FromAddress = "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should skip tx if len(tx.Vout) < 2", func(t *testing.T) { + // load tx and modify the tx to have only 1 vout + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vout = tx.Vout[:1] + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if Vout[0] is not a P2WPKH output", func(t *testing.T) { + // load tx + rpcClient := stub.NewMockBTCRPCClient() + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + + // modify the tx to have Vout[0] a P2SH output + tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + + // append 1 byte to script to make it longer than 22 bytes + tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" + event, err = GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if receiver address is not TSS address", func(t *testing.T) { + // load tx and modify receiver address to any non-tss address: bc1qw8wrek2m7nlqldll66ajnwr9mh64syvkt67zlu + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if amount is less than depositor fee", func(t *testing.T) { + // load tx and modify amount to less than depositor fee + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if 2nd vout is not OP_RETURN", func(t *testing.T) { + // load tx and modify memo OP_RETURN to OP_1 + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a", "51", 1) + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if memo decoding fails", func(t *testing.T) { + // load tx and modify memo length to be 1 byte less than actual + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a14", "6a13", 1) + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) +} + +func TestGetBtcEventErrors(t *testing.T) { + // load archived intx P2WPKH raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + chain := common.BtcMainnetChain() + net := &chaincfg.MainNetParams + tssAddress := testutils.TSSAddressBTCMainnet + blockNumber := uint64(835640) + depositorFee := DepositorFee(22 * clientcommon.BTCOuttxGasPriceMultiplier) + + t.Run("should return error on invalid Vout[0] script", func(t *testing.T) { + // load tx and modify Vout[0] script to invalid script + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vout[0].ScriptPubKey.Hex = "0014invalid000000000000000000000000000000000" + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.Error(t, err) + require.Nil(t, event) + }) + t.Run("should return error if len(tx.Vin) < 1", func(t *testing.T) { + // load tx and remove vin + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx.Vin = nil + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.Error(t, err) + require.Nil(t, event) + }) + t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { + // load tx and leave rpc client without preloaded tx + tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + rpcClient := stub.NewMockBTCRPCClient() + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.Error(t, err) + require.Nil(t, event) + }) +} diff --git a/zetaclient/bitcoin/txscript.go b/zetaclient/bitcoin/txscript.go index de344c6140..73750f1803 100644 --- a/zetaclient/bitcoin/txscript.go +++ b/zetaclient/bitcoin/txscript.go @@ -19,20 +19,12 @@ import ( ) 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" + // Length of P2TR, P2WSH, P2WPKH, P2SH, P2PKH scripts + LengthScriptP2TR = 34 + LengthScriptP2WSH = 34 + LengthScriptP2WPKH = 22 + LengthScriptP2SH = 23 + LengthScriptP2PKH = 25 ) // PayToAddrScript creates a new script to pay a transaction output to a the @@ -46,142 +38,135 @@ func PayToAddrScript(addr btcutil.Address) ([]byte, error) { } } -// 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) +// IsPkScriptP2TR checks if the given script is a P2TR script +func IsPkScriptP2TR(script []byte) bool { + // [OP_1 0x20 <32-byte-hash>] + return len(script) == LengthScriptP2TR && script[0] == txscript.OP_1 && script[1] == 0x20 +} + +// IsPkScriptP2WSH checks if the given script is a P2WSH script +func IsPkScriptP2WSH(script []byte) bool { + // [OP_0 0x20 <32-byte-hash>] + return len(script) == LengthScriptP2WSH && script[0] == txscript.OP_0 && script[1] == 0x20 +} + +// IsPkScriptP2WPKH checks if the given script is a P2WPKH script +func IsPkScriptP2WPKH(script []byte) bool { + // [OP_0 0x14 <20-byte-hash>] + return len(script) == LengthScriptP2WPKH && script[0] == txscript.OP_0 && script[1] == 0x14 +} + +// IsPkScriptP2SH checks if the given script is a P2SH script +func IsPkScriptP2SH(script []byte) bool { + // [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] + return len(script) == LengthScriptP2SH && + script[0] == txscript.OP_HASH160 && + script[1] == 0x14 && + script[22] == txscript.OP_EQUAL +} + +// IsPkScriptP2PKH checks if the given script is a P2PKH script +func IsPkScriptP2PKH(script []byte) bool { + // [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] + return len(script) == LengthScriptP2PKH && + script[0] == txscript.OP_DUP && + script[1] == txscript.OP_HASH160 && + script[2] == 0x14 && + script[23] == txscript.OP_EQUALVERIFY && + script[24] == txscript.OP_CHECKSIG +} + +// DecodeScriptP2TR decodes address from P2TR script +func DecodeScriptP2TR(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) if err != nil { - return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + return "", errors.Wrapf(err, "error decoding script %s", scriptHex) } - if len(decodedScriptPubKey) != 34 || - decodedScriptPubKey[0] != txscript.OP_1 || - decodedScriptPubKey[1] != 0x20 { - return "", fmt.Errorf("invalid P2TR scriptPubKey: %s", scriptPubKey) + if !IsPkScriptP2TR(script) { + return "", fmt.Errorf("invalid P2TR script: %s", scriptHex) } - witnessProg := decodedScriptPubKey[2:] + witnessProg := script[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 "", errors.Wrapf(err, "error getting address from script %s", scriptHex) } 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) +// DecodeScriptP2WSH decodes address from P2WSH script +func DecodeScriptP2WSH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) if err != nil { - return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) } - if len(decodedScriptPubKey) != 34 || - decodedScriptPubKey[0] != txscript.OP_0 || - decodedScriptPubKey[1] != 0x20 { - return "", fmt.Errorf("invalid P2WSH scriptPubKey: %s", scriptPubKey) + if !IsPkScriptP2WSH(script) { + return "", fmt.Errorf("invalid P2WSH script: %s", scriptHex) } - witnessProg := decodedScriptPubKey[2:] + witnessProg := script[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 "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) } 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) +// DecodeScriptP2WPKH decodes address from P2WPKH script +func DecodeScriptP2WPKH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) if err != nil { - return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) } - if len(decodedScriptPubKey) != 22 || - decodedScriptPubKey[0] != txscript.OP_0 || - decodedScriptPubKey[1] != 0x14 { - return "", fmt.Errorf("invalid P2WPKH scriptPubKey: %s", scriptPubKey) + if !IsPkScriptP2WPKH(script) { + return "", fmt.Errorf("invalid P2WPKH script: %s", scriptHex) } - witnessProg := decodedScriptPubKey[2:] + witnessProg := script[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 "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) } 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) +// DecodeScriptP2SH decodes address from P2SH script +func DecodeScriptP2SH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) if err != nil { - return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) } - 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) + if !IsPkScriptP2SH(script) { + return "", fmt.Errorf("invalid P2SH script: %s", scriptHex) } - scriptHash := decodedScriptPubKey[2:22] + scriptHash := script[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) +// DecodeScriptP2PKH decodes address from P2PKH script +func DecodeScriptP2PKH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) if err != nil { - return "", errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) } - 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) + if !IsPkScriptP2PKH(script) { + return "", fmt.Errorf("invalid P2PKH script: %s", scriptHex) } - pubKeyHash := decodedScriptPubKey[3:23] + pubKeyHash := script[3:23] return EncodeAddress(pubKeyHash, net.PubKeyHashAddrID), nil } -// DecodeVoutMemoP2WPKH decodes memo from P2WPKH output +// DecodeOpReturnMemo decodes memo from OP_RETURN script // 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) +func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { + if len(scriptHex) >= 4 && scriptHex[:2] == "6a" { // OP_RETURN + memoSize, err := strconv.ParseInt(scriptHex[2:4], 16, 32) if err != nil { - return nil, false, errors.Wrapf(err, "error decoding memo size: %s", script) + return nil, false, errors.Wrapf(err, "error decoding memo size: %s", scriptHex) } - if int(memoSize) != (len(script)-4)/2 { - return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(script)-4)/2) + if int(memoSize) != (len(scriptHex)-4)/2 { + return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(scriptHex)-4)/2) } - memoBytes, err := hex.DecodeString(script[4:]) + memoBytes, err := hex.DecodeString(scriptHex[4:]) if err != nil { - return nil, false, errors.Wrapf(err, "error hex decoding memo: %s", script) + return nil, false, errors.Wrapf(err, "error hex decoding memo: %s", scriptHex) } if bytes.Equal(memoBytes, []byte(common.DonationMessage)) { return nil, false, fmt.Errorf("donation tx: %s", txid) @@ -223,15 +208,15 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain common.Chai var receiverVout string switch addr.(type) { case *bitcoin.AddressTaproot: - receiverVout, err = DecodeVoutP2TR(vout, chainParams) + receiverVout, err = DecodeScriptP2TR(vout.ScriptPubKey.Hex, chainParams) case *btcutil.AddressWitnessScriptHash: - receiverVout, err = DecodeVoutP2WSH(vout, chainParams) + receiverVout, err = DecodeScriptP2WSH(vout.ScriptPubKey.Hex, chainParams) case *btcutil.AddressWitnessPubKeyHash: - receiverVout, err = DecodeVoutP2WPKH(vout, chainParams) + receiverVout, err = DecodeScriptP2WPKH(vout.ScriptPubKey.Hex, chainParams) case *btcutil.AddressScriptHash: - receiverVout, err = DecodeVoutP2SH(vout, chainParams) + receiverVout, err = DecodeScriptP2SH(vout.ScriptPubKey.Hex, chainParams) case *btcutil.AddressPubKeyHash: - receiverVout, err = DecodeVoutP2PKH(vout, chainParams) + receiverVout, err = DecodeScriptP2PKH(vout.ScriptPubKey.Hex, chainParams) default: return "", 0, fmt.Errorf("unsupported receiver address type: %T", addr) } diff --git a/zetaclient/bitcoin/txscript_test.go b/zetaclient/bitcoin/txscript_test.go index 57c1ebfac6..bf333ede59 100644 --- a/zetaclient/bitcoin/txscript_test.go +++ b/zetaclient/bitcoin/txscript_test.go @@ -1,6 +1,7 @@ package bitcoin import ( + "encoding/hex" "path" "strings" "testing" @@ -25,7 +26,7 @@ func TestDecodeVoutP2TR(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2TR - receiver, err := DecodeVoutP2TR(rawResult.Vout[0], net) + receiver, err := DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", receiver) } @@ -41,37 +42,31 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { 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") + _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "error decoding script") }) 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") + _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") }) 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") + _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") }) 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") + _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") }) } @@ -88,7 +83,7 @@ func TestDecodeVoutP2WSH(t *testing.T) { require.Len(t, rawResult.Vout, 1) // decode vout 0, P2WSH - receiver, err := DecodeVoutP2WSH(rawResult.Vout[0], net) + receiver, err := DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", receiver) } @@ -104,37 +99,31 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { 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") + _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "error decoding script") }) 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") + _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") }) 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") + _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") }) 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") + _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") }) } @@ -151,17 +140,17 @@ func TestDecodeP2WPKHVout(t *testing.T) { require.Len(t, rawResult.Vout, 3) // decode vout 0, nonce mark 148 - receiver, err := DecodeVoutP2WPKH(rawResult.Vout[0], net) + receiver, err := DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, 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) + receiver, err = DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) // decode vout 2, change 0.39041489 BTC - receiver, err = DecodeVoutP2WPKH(rawResult.Vout[2], net) + receiver, err = DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) } @@ -177,30 +166,24 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { 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") + _, err := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "error decoding script") }) 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") + _, err := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WPKH script") }) 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") + _, err := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WPKH script") }) } @@ -217,7 +200,7 @@ func TestDecodeVoutP2SH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2SH - receiver, err := DecodeVoutP2SH(rawResult.Vout[0], net) + receiver, err := DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", receiver) } @@ -233,43 +216,37 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { 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") + _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "error decoding script") }) 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") + _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") }) 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") + _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") }) 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") + _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") }) 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") + _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") }) } @@ -286,7 +263,7 @@ func TestDecodeVoutP2PKH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2PKH - receiver, err := DecodeVoutP2PKH(rawResult.Vout[0], net) + receiver, err := DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", receiver) } @@ -302,58 +279,126 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "error decoding script") }) 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") }) 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") }) 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") }) 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") }) 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") }) 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") + _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") + }) +} + +func TestDecodeOpReturnMemo(t *testing.T) { + // load archived intx raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + chain := common.BtcMainnetChain() + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" + rawResult := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + require.True(t, len(rawResult.Vout) >= 2) + require.Equal(t, scriptHex, rawResult.Vout[1].ScriptPubKey.Hex) + + t.Run("should decode memo from OP_RETURN output", func(t *testing.T) { + memo, found, err := DecodeOpReturnMemo(rawResult.Vout[1].ScriptPubKey.Hex, txHash) + require.NoError(t, err) + require.True(t, found) + // [OP_RETURN, 0x14,<20-byte-hash>] + require.Equal(t, scriptHex[4:], hex.EncodeToString(memo)) + }) + t.Run("should return nil memo non-OP_RETURN output", func(t *testing.T) { + // modify the OP_RETURN to OP_1 + scriptInvalid := strings.Replace(scriptHex, "6a", "51", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.NoError(t, err) + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return nil memo on invalid script", func(t *testing.T) { + // use known short script + scriptInvalid := "00" + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.NoError(t, err) + require.False(t, found) + require.Nil(t, memo) + }) +} + +func TestDecodeOpReturnMemoErrors(t *testing.T) { + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" + + t.Run("should return error on invalid memo size", func(t *testing.T) { + // use invalid memo size + scriptInvalid := strings.Replace(scriptHex, "6a14", "6axy", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.ErrorContains(t, err, "error decoding memo size") + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return error on memo size mismatch", func(t *testing.T) { + // use wrong memo size + scriptInvalid := strings.Replace(scriptHex, "6a14", "6a13", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.ErrorContains(t, err, "memo size mismatch") + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return error on invalid hex", func(t *testing.T) { + // use invalid hex + scriptInvalid := strings.Replace(scriptHex, "6a1467", "6a14xy", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.ErrorContains(t, err, "error hex decoding memo") + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return nil memo on donation tx", func(t *testing.T) { + // use donation sctipt "6a0a4920616d207269636821" + scriptDonation := "6a0a" + hex.EncodeToString([]byte(common.DonationMessage)) + memo, found, err := DecodeOpReturnMemo(scriptDonation, txHash) + require.ErrorContains(t, err, "donation tx") + require.False(t, found) + require.Nil(t, memo) }) } diff --git a/zetaclient/interfaces/interfaces.go b/zetaclient/interfaces/interfaces.go index 5842c2b944..40a16e0df1 100644 --- a/zetaclient/interfaces/interfaces.go +++ b/zetaclient/interfaces/interfaces.go @@ -122,6 +122,7 @@ type BTCRPCClient interface { ListUnspentMinMaxAddresses(minConf int, maxConf int, addrs []btcutil.Address) ([]btcjson.ListUnspentResult, error) EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) + GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) GetBlockCount() (int64, error) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) diff --git a/zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json b/zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json deleted file mode 100644 index ae1b750542..0000000000 --- a/zetaclient/testdata/btc/chain_8332_intx_raw_result_8a2f9d30466d4f4fb245a918d5d7ad049d49719c3493e6495c88f969bae1c570.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "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_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json b/zetaclient/testdata/btc/chain_8332_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json new file mode 100644 index 0000000000..6dcd43a643 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json @@ -0,0 +1,25 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 30, 42, 54, 220, 104, 77, 18, 96, 207, 100, 83, 218, 222, 30, 81, 215, + 17, 216, 241, 23, 140, 26, 84, 197, 241, 73, 15, 53, 249, 182, 38, 134 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEUCIQDJuzu34uVBPrsi8DgNfQH5TwM2uX//dXWkULGhElDYzAIgYxSUnwOnyHIBR546z56in6he+zNI5xMEso/0szFIy7QB", + "Am5WKFBuzTMkLlzrX9r+TTBmtcDxWbPAWmIe9l8XfqKG" + ], + "Sequence": 4294967293 + } + ], + "TxOut": [ + { "Value": 249140, "PkScript": "qRTc+XVrIoqedsOz7aKs1GSUk2FjrYc=" }, + { "Value": 965576774, "PkScript": "ABT2CDTvFlJTxXGxHOn6dORmkvxewQ==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json b/zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json new file mode 100644 index 0000000000..15152341cd --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json @@ -0,0 +1,29 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 116, 197, 95, 238, 197, 226, 79, 77, 64, 228, 72, 250, 109, 241, 81, + 234, 21, 9, 136, 241, 107, 167, 93, 253, 169, 243, 200, 138, 214, 113, + 80, 155 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "nmIsIhEBkmvAE0tWRBnjha0XCBGEQqVn1RSDVYyQNZSb7MkfyEOG+isbESaOA14udWx8Hm8kjHaQz61GngwUEg==" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { "Value": 0, "PkScript": "aha/loYCLBy+fQfY5HO29OG9kYEctgAA" }, + { "Value": 10850, "PkScript": "ABTaquDT3p2P3uMWYeYa6oKLWb54ZA==" }, + { + "Value": 82110, + "PkScript": "USA0Q5Bhun3t5wCAtHWi+4W4uIA8LafEyeSFiQQhgJVNgg==" + } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json b/zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json new file mode 100644 index 0000000000..2d5708f83d --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json @@ -0,0 +1 @@ +{"Version":1,"TxIn":[{"PreviousOutPoint":{"Hash":[88,35,68,215,98,226,80,135,144,112,176,41,10,204,196,243,171,206,47,130,190,82,88,31,234,63,168,65,219,15,248,81],"Index":1},"SignatureScript":"SDBFAiEAtm10q/gt84o5QCkWsxouad5aqiEDgBd3h3ci/8tuDl4CIB9wKaSz2HDRhPKwkoXBbYqZwTD+8fhiZOolJcwvgV49ASED25w7Nl3+PHENu2OrUD2EmDvsdylmoWe0hSDOMtQvj04=","Witness":null,"Sequence":268435456}],"TxOut":[{"Value":1425000,"PkScript":"qRRdKyXqq2FtT8BTLay8+A2Un6cUTIc="},{"Value":19064706,"PkScript":"dqkUk2fpBSIxBUekNfA/gxxYw1RZ3KGIrA=="}],"LockTime":0} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json b/zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json new file mode 100644 index 0000000000..1d0004422e --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json @@ -0,0 +1,42 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 127, 3, 175, 215, 135, 178, 52, 169, 234, 18, 205, 65, 18, 193, 182, + 123, 2, 102, 254, 107, 221, 18, 243, 92, 98, 226, 161, 209, 185, 140, + 72, 91 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEUCIQCWzKD96dT53Y9o+KCtLvB5WXrJL4X2XGgpiz3rRBdJMwIgcdwc7Z870dOeiVUZdSLjRAJ8hkLz0l7TXVSLF0ZArekB", + "A1zjZr/QH950JWL3zF5q4SXsK6yGLTw6EdLXCxs7qprg" + ], + "Sequence": 4294967295 + }, + { + "PreviousOutPoint": { + "Hash": [ + 175, 165, 125, 14, 98, 13, 62, 204, 88, 160, 18, 74, 177, 75, 159, 44, + 131, 4, 151, 145, 200, 2, 177, 50, 227, 26, 2, 89, 184, 155, 179, 54 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEQCIGbWmCHGFlrSKF7+tnQHhrQGDNS0PR2E4lxq/OLz68CFAiB/yHWTSaAs1GbDzngvzFkGu76N/YKJX1iy88irfDg0tAE=", + "A1zjZr/QH950JWL3zF5q4SXsK6yGLTw6EdLXCxs7qprg" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { "Value": 1000, "PkScript": "ABTaquDT3p2P3uMWYeYa6oKLWb54ZA==" }, + { "Value": 0, "PkScript": "ahRn7QvMThJWvCzofSLhkNY6EgEUvw==" }, + { "Value": 89858, "PkScript": "ABTR7GmCiu3FRjdYPgWWQ67j+GLNIw==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json b/zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json new file mode 100644 index 0000000000..e7343425a4 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json @@ -0,0 +1,28 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 121, 61, 82, 212, 158, 94, 66, 22, 92, 111, 162, 24, 255, 33, 79, 208, + 234, 255, 133, 0, 255, 110, 97, 104, 105, 49, 187, 30, 61, 168, 9, 166 + ], + "Index": 1 + }, + "SignatureScript": "", + "Witness": [ + "MEQCIDeXH61fcBuKz091+3kS2ZETW4hjkbfDIxRcnVU+TrIZAiBBg9boZh2AcAa5HIXpKS5YSfDLsJz817yDC5lPEnzYLAE=", + "AxAyc+pMvZfG+8ELyKyr2TBo0kulM0XM4QisuLyR7L3f" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { + "Value": 50000000, + "PkScript": "ACDxbbwTHn6bqd/LCK85n+IPtDC9eQhvG4HDv3AE4JX7jA==" + }, + { "Value": 9992996, "PkScript": "ABT8w6bf+cf2lmZLrUWPhgVJeNjEVw==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testutils/stub/btc_rpc.go b/zetaclient/testutils/stub/btc_rpc.go new file mode 100644 index 0000000000..6b8ac84c53 --- /dev/null +++ b/zetaclient/testutils/stub/btc_rpc.go @@ -0,0 +1,121 @@ +package stub + +import ( + "errors" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" +) + +// EvmClient interface +var _ interfaces.BTCRPCClient = &MockBTCRPCClient{} + +// MockBTCRPCClient is a mock implementation of the BTCRPCClient interface +type MockBTCRPCClient struct { + Txs []*btcutil.Tx +} + +// NewMockBTCRPCClient creates a new mock BTC RPC client +func NewMockBTCRPCClient() *MockBTCRPCClient { + client := &MockBTCRPCClient{} + return client.Reset() +} + +// Reset clears the mock data +func (c *MockBTCRPCClient) Reset() *MockBTCRPCClient { + c.Txs = []*btcutil.Tx{} + return c +} + +func (c *MockBTCRPCClient) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) CreateWallet(_ string, _ ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetNewAddress(_ string) (btcutil.Address, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GenerateToAddress(_ int64, _ btcutil.Address, _ *int64) ([]*chainhash.Hash, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBalance(_ string) (btcutil.Amount, error) { + return 0, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) SendRawTransaction(_ *wire.MsgTx, _ bool) (*chainhash.Hash, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) ListUnspent() ([]btcjson.ListUnspentResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) ListUnspentMinMaxAddresses(_ int, _ int, _ []btcutil.Address) ([]btcjson.ListUnspentResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) EstimateSmartFee(_ int64, _ *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetTransaction(_ *chainhash.Hash) (*btcjson.GetTransactionResult, error) { + return nil, errors.New("not implemented") +} + +// GetRawTransaction returns a pre-loaded transaction or nil +func (c *MockBTCRPCClient) GetRawTransaction(_ *chainhash.Hash) (*btcutil.Tx, error) { + // pop a transaction from the list + if len(c.Txs) > 0 { + tx := c.Txs[len(c.Txs)-1] + c.Txs = c.Txs[:len(c.Txs)-1] + return tx, nil + } + return nil, errors.New("no transaction found") +} + +func (c *MockBTCRPCClient) GetRawTransactionVerbose(_ *chainhash.Hash) (*btcjson.TxRawResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockCount() (int64, error) { + return 0, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockHash(_ int64) (*chainhash.Hash, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockVerbose(_ *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockVerboseTx(_ *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockHeader(_ *chainhash.Hash) (*wire.BlockHeader, error) { + return nil, errors.New("not implemented") +} + +// ---------------------------------------------------------------------------- +// Feed data to the mock BTC RPC client for testing +// ---------------------------------------------------------------------------- + +func (c *MockBTCRPCClient) WithRawTransaction(tx *btcutil.Tx) *MockBTCRPCClient { + c.Txs = append(c.Txs, tx) + return c +} + +func (c *MockBTCRPCClient) WithRawTransactions(txs []*btcutil.Tx) *MockBTCRPCClient { + c.Txs = append(c.Txs, txs...) + return c +} diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index 11d12694f7..79311fdf52 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -87,6 +87,14 @@ func LoadEVMBlock(chainID int64, blockNumber uint64, trimmed bool) *ethrpc.Block return block } +// LoadBTCIntxRawResult loads archived Bitcoin intx raw result from file +func LoadBTCIntxRawResult(chainID int64, txHash string, donation bool) *btcjson.TxRawResult { + name := path.Join("../", TestDataPathBTC, FileNameBTCIntx(chainID, txHash, donation)) + rawResult := &btcjson.TxRawResult{} + LoadObjectFromJSONFile(rawResult, name) + return rawResult +} + // LoadBTCTxRawResultNCctx loads archived Bitcoin outtx raw result and corresponding cctx func LoadBTCTxRawResultNCctx(chainID int64, nonce uint64) (*btcjson.TxRawResult, *crosschaintypes.CrossChainTx) { //nameTx := FileNameBTCOuttx(chainID, nonce) diff --git a/zetaclient/testutils/testdata_naming.go b/zetaclient/testutils/testdata_naming.go index b417435237..eb4516f4d0 100644 --- a/zetaclient/testutils/testdata_naming.go +++ b/zetaclient/testutils/testdata_naming.go @@ -54,6 +54,11 @@ func FileNameBTCTxByType(chainID int64, txType string, txHash string) string { return fmt.Sprintf("chain_%d_tx_raw_result_%s_%s.json", chainID, txType, txHash) } +// FileNameBTCMsgTx returns unified archive file name for btc MsgTx +func FileNameBTCMsgTx(chainID int64, txHash string) string { + return fmt.Sprintf("chain_%d_msgtx_%s.json", chainID, 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) From 18fc62f12c624e234352fab0e0a0dd17b83f6d11 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 25 Mar 2024 10:46:38 -0500 Subject: [PATCH 07/16] fix compile error --- .../evm/outbound_transaction_data_test.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/zetaclient/evm/outbound_transaction_data_test.go b/zetaclient/evm/outbound_transaction_data_test.go index cc5de0489b..abf9656665 100644 --- a/zetaclient/evm/outbound_transaction_data_test.go +++ b/zetaclient/evm/outbound_transaction_data_test.go @@ -13,9 +13,7 @@ import ( func TestSigner_SetChainAndSender(t *testing.T) { // setup inputs - cctx, err := getCCTX() - require.NoError(t, err) - + cctx := getCCTX() txData := &OutBoundTransactionData{} logger := zerolog.Logger{} @@ -45,9 +43,7 @@ func TestSigner_SetChainAndSender(t *testing.T) { } func TestSigner_SetupGas(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) - + cctx := getCCTX() evmSigner, err := getNewEvmSigner() require.NoError(t, err) @@ -77,16 +73,14 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { require.NoError(t, err) t.Run("NewOutBoundTransactionData success", func(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) }) t.Run("NewOutBoundTransactionData skip", func(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX() cctx.CctxStatus.Status = types.CctxStatus_Aborted _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.NoError(t, err) @@ -94,7 +88,7 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { }) t.Run("NewOutBoundTransactionData unknown chain", func(t *testing.T) { - cctx, err := getInvalidCCTX() + cctx := getInvalidCCTX() require.NoError(t, err) _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.ErrorContains(t, err, "unknown chain") @@ -102,7 +96,7 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { }) t.Run("NewOutBoundTransactionData setup gas error", func(t *testing.T) { - cctx, err := getCCTX() + cctx := getCCTX() require.NoError(t, err) cctx.GetCurrentOutTxParam().OutboundTxGasPrice = "invalidGasPrice" _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) From 76b6817cfc7f1c93eb17a589b40582178ed20af4 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 26 Mar 2024 00:48:38 -0500 Subject: [PATCH 08/16] adjusted btc outtx size and withdrawer fee --- zetaclient/bitcoin/bitcoin_signer.go | 9 +- zetaclient/bitcoin/bitcoin_signer_test.go | 214 +---------- zetaclient/bitcoin/bitcoin_test.go | 1 - zetaclient/bitcoin/fee.go | 234 ++++++++++++ zetaclient/bitcoin/fee_test.go | 412 ++++++++++++++++++++++ zetaclient/bitcoin/utils.go | 173 --------- zetaclient/evm/evm_signer_test.go | 5 +- 7 files changed, 654 insertions(+), 394 deletions(-) create mode 100644 zetaclient/bitcoin/fee.go create mode 100644 zetaclient/bitcoin/fee_test.go diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index ea85dcb3a1..83ef32e7a4 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -32,10 +32,11 @@ import ( ) const ( + // the maximum number of inputs per outtx maxNoOfInputsPerTx = 20 - consolidationRank = 10 // the rank below (or equal to) which we consolidate UTXOs - outTxBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 3) - outTxBytesMax = uint64(1531) // 1531v == EstimateSegWitTxSize(21, 3) + + // the rank below (or equal to) which we consolidate UTXOs + consolidationRank = 10 ) // BTCSigner deals with signing BTC transactions and implements the ChainSigner interface @@ -199,7 +200,7 @@ func (signer *BTCSigner) SignWithdrawTx( // size checking // #nosec G701 always positive - txSize := EstimateSegWitTxSize(uint64(len(prevOuts)), 3) + txSize := EstimateOuttxSize(uint64(len(prevOuts)), []btcutil.Address{to}) if sizeLimit < BtcOutTxBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user signer.logger.Info().Msgf("sizeLimit %d is less than BtcOutTxBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce) } diff --git a/zetaclient/bitcoin/bitcoin_signer_test.go b/zetaclient/bitcoin/bitcoin_signer_test.go index 637ee3ff3d..575b8f0d09 100644 --- a/zetaclient/bitcoin/bitcoin_signer_test.go +++ b/zetaclient/bitcoin/bitcoin_signer_test.go @@ -5,13 +5,11 @@ import ( "fmt" "math" "math/big" - "math/rand" "reflect" "sort" "sync" "testing" - "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" @@ -37,31 +35,6 @@ type BTCSignerSuite struct { var _ = Suite(&BTCSignerSuite{}) -// 21 example UTXO txids to use in the test. -var exampleTxids = []string{ - "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", - "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", - "b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc", - "969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de", - "6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e", - "ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585", - "69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33", - "b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf", - "3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda", - "8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e", - "f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a", - "c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933", - "ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b", - "61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a", - "ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525", - "b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981", - "185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482", - "4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55", - "fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef", - "7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3", - "6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326", -} - func (s *BTCSignerSuite) SetUpTest(c *C) { // test private key with EVM address //// EVM: 0x236C7f53a90493Bb423411fe4117Cb4c2De71DfB @@ -242,192 +215,6 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { fmt.Println("Transaction successfully signed") } -func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, []byte) { - privateKey, err := btcec.NewPrivateKey(btcec.S256()) - require.Nil(t, err) - pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed()) - addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) - require.Nil(t, err) - //fmt.Printf("New address: %s\n", addr.EncodeAddress()) - pkScript, err := PayToAddrScript(addr) - require.Nil(t, err) - return privateKey, pkScript -} - -func addTxInputs(t *testing.T, tx *wire.MsgTx, txids []string) { - preTxSize := tx.SerializeSize() - for _, txid := range txids { - hash, err := chainhash.NewHashFromStr(txid) - require.Nil(t, err) - outpoint := wire.NewOutPoint(hash, uint32(rand.Intn(100))) - txIn := wire.NewTxIn(outpoint, nil, nil) - tx.AddTxIn(txIn) - require.Equal(t, bytesPerInput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, input %d size: %d\n", tx.SerializeSize(), i, tx.SerializeSize()-preTxSize) - preTxSize = tx.SerializeSize() - } -} - -func addTxOutputs(t *testing.T, tx *wire.MsgTx, payerScript, payeeScript []byte) { - preTxSize := tx.SerializeSize() - - // 1st output to payer - value1 := int64(1 + rand.Intn(100000000)) - txOut1 := wire.NewTxOut(value1, payerScript) - tx.AddTxOut(txOut1) - require.Equal(t, bytesPerOutput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, output 1: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) - preTxSize = tx.SerializeSize() - - // 2nd output to payee - value2 := int64(1 + rand.Intn(100000000)) - txOut2 := wire.NewTxOut(value2, payeeScript) - tx.AddTxOut(txOut2) - require.Equal(t, bytesPerOutput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, output 2: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) - preTxSize = tx.SerializeSize() - - // 3rd output to payee - value3 := int64(1 + rand.Intn(100000000)) - txOut3 := wire.NewTxOut(value3, payeeScript) - tx.AddTxOut(txOut3) - require.Equal(t, bytesPerOutput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, output 3: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) -} - -func signTx(t *testing.T, tx *wire.MsgTx, payerScript []byte, privateKey *btcec.PrivateKey) { - preTxSize := tx.SerializeSize() - sigHashes := txscript.NewTxSigHashes(tx) - for ix := range tx.TxIn { - amount := int64(1 + rand.Intn(100000000)) - witnessHash, err := txscript.CalcWitnessSigHash(payerScript, sigHashes, txscript.SigHashAll, tx, ix, amount) - require.Nil(t, err) - sig, err := privateKey.Sign(witnessHash) - require.Nil(t, err) - - pkCompressed := privateKey.PubKey().SerializeCompressed() - txWitness := wire.TxWitness{append(sig.Serialize(), byte(txscript.SigHashAll)), pkCompressed} - tx.TxIn[ix].Witness = txWitness - - //fmt.Printf("tx size: %d, witness %d: %d\n", tx.SerializeSize(), ix+1, tx.SerializeSize()-preTxSize) - if ix == 0 { - bytesIncur := bytes1stWitness + len(tx.TxIn) - 1 // e.g., 130 bytes for a 21-input tx - require.True(t, tx.SerializeSize()-preTxSize >= bytesIncur-5) - require.True(t, tx.SerializeSize()-preTxSize <= bytesIncur+5) - } else { - require.True(t, tx.SerializeSize()-preTxSize >= bytesPerWitness-5) - require.True(t, tx.SerializeSize()-preTxSize <= bytesPerWitness+5) - } - preTxSize = tx.SerializeSize() - } -} - -func TestP2WPKHSize2In3Out(t *testing.T) { - // Generate payer/payee private keys and P2WPKH addresss - privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) - _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - - // 2 example UTXO txids to use in the test. - utxosTxids := []string{ - "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", - "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", - } - - // Create a new transaction and add inputs - tx := wire.NewMsgTx(wire.TxVersion) - addTxInputs(t, tx, utxosTxids) - - // Add P2WPKH outputs - addTxOutputs(t, tx, payerScript, payeeScript) - - // Payer sign the redeeming transaction. - signTx(t, tx, payerScript, privateKey) - - // Estimate the tx size in vByte - // #nosec G701 always positive - vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3) - require.Equal(t, vBytes, vBytesEstimated) - require.Equal(t, vBytes, outTxBytesMin) -} - -func TestP2WPKHSize21In3Out(t *testing.T) { - // Generate payer/payee private keys and P2WPKH addresss - privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) - _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - - // Create a new transaction and add inputs - tx := wire.NewMsgTx(wire.TxVersion) - addTxInputs(t, tx, exampleTxids) - - // Add P2WPKH outputs - addTxOutputs(t, tx, payerScript, payeeScript) - - // Payer sign the redeeming transaction. - signTx(t, tx, payerScript, privateKey) - - // Estimate the tx size in vByte - // #nosec G701 always positive - vError := uint64(21 / 4) // 5 vBytes error tolerance - vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids)), 3) - require.Equal(t, vBytesEstimated, outTxBytesMax) - if vBytes > vBytesEstimated { - require.True(t, vBytes-vBytesEstimated <= vError) - } else { - require.True(t, vBytesEstimated-vBytes <= vError) - } -} - -func TestP2WPKHSizeXIn3Out(t *testing.T) { - // Generate payer/payee private keys and P2WPKH addresss - privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) - _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - - // Create new transactions with X (2 <= X <= 21) inputs and 3 outputs respectively - for x := 2; x <= 21; x++ { - tx := wire.NewMsgTx(wire.TxVersion) - addTxInputs(t, tx, exampleTxids[:x]) - - // Add P2WPKH outputs - addTxOutputs(t, tx, payerScript, payeeScript) - - // Payer sign the redeeming transaction. - signTx(t, tx, payerScript, privateKey) - - // Estimate the tx size - // #nosec G701 always positive - vError := uint64(0.25 + float64(x)/4) // 1st witness incur 0.25 vByte error, other witness incur 1/4 vByte error tolerance, - vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids[:x])), 3) - if vBytes > vBytesEstimated { - require.True(t, vBytes-vBytesEstimated <= vError) - //fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100) - } else { - require.True(t, vBytesEstimated-vBytes <= vError) - //fmt.Printf("error percentage: %.2f%%\n", float64(vBytesEstimated-vBytes)/float64(vBytes)*100) - } - } -} - -func TestP2WPKHSizeBreakdown(t *testing.T) { - txSize2In3Out := EstimateSegWitTxSize(2, 3) - require.Equal(t, outTxBytesMin, txSize2In3Out) - - sz := EstimateSegWitTxSize(1, 1) - fmt.Printf("1 input, 1 output: %d\n", sz) - - txSizeDepositor := SegWitTxSizeDepositor() - require.Equal(t, uint64(68), txSizeDepositor) - - txSizeWithdrawer := SegWitTxSizeWithdrawer() - require.Equal(t, uint64(171), txSizeWithdrawer) - require.Equal(t, txSize2In3Out, txSizeDepositor+txSizeWithdrawer) // 239 = 68 + 171 - - depositFee := DepositorFee(defaultDepositorFeeRate) - require.Equal(t, depositFee, 0.00001360) -} - // helper function to create a test BitcoinChainClient func createTestClient(t *testing.T) *BTCChainClient { skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" @@ -825,5 +612,6 @@ func TestNewBTCSigner(t *testing.T) { clientcommon.DefaultLoggers(), &metrics.TelemetryServer{}, corecontext.NewZetaCoreContext(cfg)) + require.NoError(t, err) require.NotNil(t, btcSigner) } diff --git a/zetaclient/bitcoin/bitcoin_test.go b/zetaclient/bitcoin/bitcoin_test.go index de05dcf543..3faa109695 100644 --- a/zetaclient/bitcoin/bitcoin_test.go +++ b/zetaclient/bitcoin/bitcoin_test.go @@ -96,7 +96,6 @@ func buildTX() (*wire.MsgTx, *txscript.TxSigHashes, int, int64, []byte, *btcec.P if err != nil { return nil, nil, 0, 0, nil, nil, false, err } - fmt.Printf("addr %v\n", addr.EncodeAddress()) hash, err := chainhash.NewHashFromStr(prevOut) if err != nil { diff --git a/zetaclient/bitcoin/fee.go b/zetaclient/bitcoin/fee.go new file mode 100644 index 0000000000..59017448ce --- /dev/null +++ b/zetaclient/bitcoin/fee.go @@ -0,0 +1,234 @@ +package bitcoin + +import ( + "encoding/hex" + "fmt" + "math" + "math/big" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/common/bitcoin" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + + "github.com/btcsuite/btcd/wire" + "github.com/pkg/errors" +) + +const ( + bytesPerKB = 1000 + bytesEmptyTx = 10 // an empty tx is 10 bytes + bytesPerInput = 41 // each input is 41 bytes + bytesPerOutputP2TR = 43 // each P2TR output is 43 bytes + bytesPerOutputP2WSH = 43 // each P2WSH output is 43 bytes + bytesPerOutputP2WPKH = 31 // each P2WPKH output is 31 bytes + bytesPerOutputP2SH = 32 // each P2SH output is 32 bytes + bytesPerOutputP2PKH = 34 // each P2PKH output is 34 bytes + bytesPerOutputAvg = 37 // average size of all above types of outputs (36.6 bytes) + bytes1stWitness = 110 // the 1st witness incurs about 110 bytes and it may vary + bytesPerWitness = 108 // each additional witness incurs about 108 bytes and it may vary + defaultDepositorFeeRate = 20 // 20 sat/byte is the default depositor fee rate + + outTxBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 2, toP2WPKH) + outTxBytesMax = uint64(1543) // 1543v == EstimateSegWitTxSize(21, 2, toP2TR) + outTxBytesAvg = uint64(245) // 245vB is a suggested gas limit for zeta core +) + +var ( + BtcOutTxBytesDepositor uint64 + BtcOutTxBytesWithdrawer uint64 + DefaultDepositorFee float64 +) + +func init() { + BtcOutTxBytesDepositor = OuttxSizeDepositor() // 68vB, the outtx size incurred by the depositor + BtcOutTxBytesWithdrawer = OuttxSizeWithdrawer() // 177vB, the outtx size incurred by the withdrawer + + // default depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity. + // In reality, the fee rate on UTXO deposit is different from the fee rate when the UTXO is spent. + DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) // 0.00001360 (20 * 68vB / 100000000) +} + +// FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte. +func FeeRateToSatPerByte(rate float64) *big.Int { + // #nosec G701 always in range + satPerKB := new(big.Int).SetInt64(int64(rate * btcutil.SatoshiPerBitcoin)) + return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB)) +} + +// WiredTxSize calculates the wired tx size in bytes +func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 { + // Version 4 bytes + LockTime 4 bytes + Serialized varint size for the + // number of transaction inputs and outputs. + // #nosec G701 always positive + return uint64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs)) +} + +// EstimateOuttxSize estimates the size of a outtx in vBytes +func EstimateOuttxSize(numInputs uint64, payees []btcutil.Address) uint64 { + if numInputs == 0 { + return 0 + } + // #nosec G701 always positive + numOutputs := 2 + uint64(len(payees)) + bytesWiredTx := WiredTxSize(numInputs, numOutputs) + bytesInput := numInputs * bytesPerInput + bytesOutput := uint64(2) * bytesPerOutputP2WPKH // new nonce mark, change + + // calculate the size of the outputs to payees + bytesToPayees := uint64(0) + for _, to := range payees { + bytesToPayees += GetOutputSizeByAddress(to) + } + // calculate the size of the witness + bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness + // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations + // Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4 + return bytesWiredTx + bytesInput + bytesOutput + bytesToPayees + bytesWitness/blockchain.WitnessScaleFactor +} + +// GetOutputSizeByAddress returns the size of a tx output in bytes by the given address +func GetOutputSizeByAddress(to btcutil.Address) uint64 { + switch addr := to.(type) { + case *bitcoin.AddressTaproot: + if addr == nil { + return 0 + } + return bytesPerOutputP2TR + case *btcutil.AddressWitnessScriptHash: + if addr == nil { + return 0 + } + return bytesPerOutputP2WSH + case *btcutil.AddressWitnessPubKeyHash: + if addr == nil { + return 0 + } + return bytesPerOutputP2WPKH + case *btcutil.AddressScriptHash: + if addr == nil { + return 0 + } + return bytesPerOutputP2SH + case *btcutil.AddressPubKeyHash: + if addr == nil { + return 0 + } + return bytesPerOutputP2PKH + default: + return bytesPerOutputP2WPKH + } +} + +// OuttxSizeDepositor returns outtx size (68vB) incurred by the depositor +func OuttxSizeDepositor() uint64 { + return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor +} + +// OuttxSizeWithdrawer returns outtx size (177vB) incurred by the withdrawer (1 input, 3 outputs) +func OuttxSizeWithdrawer() uint64 { + bytesWiredTx := WiredTxSize(1, 3) + bytesInput := uint64(1) * bytesPerInput // nonce mark + bytesOutput := uint64(2) * bytesPerOutputP2WPKH // 2 P2WPKH outputs: new nonce mark, change + bytesOutput += bytesPerOutputAvg // 1 output to withdrawer's address + return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor +} + +// DepositorFee calculates the depositor fee in BTC for a given sat/byte fee rate +// Note: the depositor fee is charged in order to cover the cost of spending the deposited UTXO in the future +func DepositorFee(satPerByte int64) float64 { + return float64(satPerByte) * float64(BtcOutTxBytesDepositor) / btcutil.SatoshiPerBitcoin +} + +// CalcBlockAvgFeeRate calculates the average gas rate (in sat/vByte) for a given block +func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *chaincfg.Params) (int64, error) { + // sanity check + if len(blockVb.Tx) == 0 { + return 0, errors.New("block has no transactions") + } + if len(blockVb.Tx) == 1 { + return 0, nil // only coinbase tx, it happens + } + txCoinbase := &blockVb.Tx[0] + if blockVb.Weight < blockchain.WitnessScaleFactor { + return 0, fmt.Errorf("block weight %d too small", blockVb.Weight) + } + if blockVb.Weight < txCoinbase.Weight { + return 0, fmt.Errorf("block weight %d less than coinbase tx weight %d", blockVb.Weight, txCoinbase.Weight) + } + if blockVb.Height <= 0 || blockVb.Height > math.MaxInt32 { + return 0, fmt.Errorf("invalid block height %d", blockVb.Height) + } + + // make sure the first tx is coinbase tx + txBytes, err := hex.DecodeString(txCoinbase.Hex) + if err != nil { + return 0, fmt.Errorf("failed to decode coinbase tx %s", txCoinbase.Txid) + } + tx, err := btcutil.NewTxFromBytes(txBytes) + if err != nil { + return 0, fmt.Errorf("failed to parse coinbase tx %s", txCoinbase.Txid) + } + if !blockchain.IsCoinBaseTx(tx.MsgTx()) { + return 0, fmt.Errorf("first tx %s is not coinbase tx", txCoinbase.Txid) + } + + // calculate fees earned by the miner + btcEarned := int64(0) + for _, out := range tx.MsgTx().TxOut { + if out.Value > 0 { + btcEarned += out.Value + } + } + // #nosec G701 checked above + subsidy := blockchain.CalcBlockSubsidy(int32(blockVb.Height), netParams) + if btcEarned < subsidy { + return 0, fmt.Errorf("miner earned %d, less than subsidy %d", btcEarned, subsidy) + } + txsFees := btcEarned - subsidy + + // sum up weight of all txs (<= 4 MWU) + txsWeight := int32(0) + for i, tx := range blockVb.Tx { + // coinbase doesn't pay fees, so we exclude it + if i > 0 && tx.Weight > 0 { + txsWeight += tx.Weight + } + } + + // calculate average fee rate. + vBytes := txsWeight / blockchain.WitnessScaleFactor + return txsFees / int64(vBytes), nil +} + +// CalcDepositorFee calculates the depositor fee for a given block +func CalcDepositorFee(blockVb *btcjson.GetBlockVerboseTxResult, chainID int64, netParams *chaincfg.Params, logger zerolog.Logger) float64 { + // use dynamic fee or default + dynamicFee := true + + // use default fee for regnet + if common.IsBitcoinRegnet(chainID) { + dynamicFee = false + } + // mainnet dynamic fee takes effect only after a planned upgrade height + if common.IsBitcoinMainnet(chainID) && blockVb.Height < DynamicDepositorFeeHeight { + dynamicFee = false + } + if !dynamicFee { + return DefaultDepositorFee + } + + // calculate deposit fee rate + feeRate, err := CalcBlockAvgFeeRate(blockVb, netParams) + if err != nil { + feeRate = defaultDepositorFeeRate // use default fee rate if calculation fails, should not happen + logger.Error().Err(err).Msgf("cannot calculate fee rate for block %d", blockVb.Height) + } + // #nosec G701 always in range + feeRate = int64(float64(feeRate) * clientcommon.BTCOuttxGasPriceMultiplier) + return DepositorFee(feeRate) +} diff --git a/zetaclient/bitcoin/fee_test.go b/zetaclient/bitcoin/fee_test.go new file mode 100644 index 0000000000..fb14f08c2d --- /dev/null +++ b/zetaclient/bitcoin/fee_test.go @@ -0,0 +1,412 @@ +package bitcoin + +import ( + "math/rand" + "testing" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/common/bitcoin" +) + +const ( + // btc address script types + ScriptTypeP2TR = "witness_v1_taproot" + ScriptTypeP2WSH = "witness_v0_scripthash" + ScriptTypeP2WPKH = "witness_v0_keyhash" + ScriptTypeP2SH = "scripthash" + ScriptTypeP2PKH = "pubkeyhash" +) + +var testAddressMap = map[string]string{ + ScriptTypeP2TR: "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", + ScriptTypeP2WSH: "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", + ScriptTypeP2WPKH: "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y", + ScriptTypeP2SH: "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", + ScriptTypeP2PKH: "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", +} + +// 21 example UTXO txids to use in the test. +var exampleTxids = []string{ + "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", + "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", + "b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc", + "969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de", + "6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e", + "ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585", + "69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33", + "b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf", + "3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda", + "8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e", + "f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a", + "c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933", + "ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b", + "61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a", + "ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525", + "b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981", + "185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482", + "4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55", + "fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef", + "7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3", + "6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326", +} + +func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, btcutil.Address, []byte) { + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + require.Nil(t, err) + pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed()) + addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) + require.Nil(t, err) + //fmt.Printf("New address: %s\n", addr.EncodeAddress()) + pkScript, err := PayToAddrScript(addr) + require.Nil(t, err) + return privateKey, addr, pkScript +} + +// getTestAddrScript returns hard coded test address scripts by script type +func getTestAddrScript(t *testing.T, scriptType string) btcutil.Address { + chain := common.BtcMainnetChain() + if inputAddress, ok := testAddressMap[scriptType]; ok { + address, err := common.DecodeBtcAddress(inputAddress, chain.ChainId) + require.NoError(t, err) + return address + } else { + panic("unknown script type") + } +} + +// createPkScripts creates 10 random amount of scripts to the given address 'to' +func createPkScripts(t *testing.T, to btcutil.Address, repeat int) ([]btcutil.Address, [][]byte) { + pkScript, err := PayToAddrScript(to) + require.NoError(t, err) + + addrs := []btcutil.Address{} + pkScripts := [][]byte{} + for i := 0; i < repeat; i++ { + addrs = append(addrs, to) + pkScripts = append(pkScripts, pkScript) + } + return addrs, pkScripts +} + +func addTxInputs(t *testing.T, tx *wire.MsgTx, txids []string) { + preTxSize := tx.SerializeSize() + for _, txid := range txids { + hash, err := chainhash.NewHashFromStr(txid) + require.Nil(t, err) + outpoint := wire.NewOutPoint(hash, uint32(rand.Intn(100))) + txIn := wire.NewTxIn(outpoint, nil, nil) + tx.AddTxIn(txIn) + require.Equal(t, bytesPerInput, tx.SerializeSize()-preTxSize) + //fmt.Printf("tx size: %d, input %d size: %d\n", tx.SerializeSize(), i, tx.SerializeSize()-preTxSize) + preTxSize = tx.SerializeSize() + } +} + +func addTxOutputs(t *testing.T, tx *wire.MsgTx, payerScript []byte, payeeScripts [][]byte) { + preTxSize := tx.SerializeSize() + + // 1st output to payer + value1 := int64(1 + rand.Intn(100000000)) + txOut1 := wire.NewTxOut(value1, payerScript) + tx.AddTxOut(txOut1) + require.Equal(t, bytesPerOutputP2WPKH, tx.SerializeSize()-preTxSize) + //fmt.Printf("tx size: %d, output 1: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) + preTxSize = tx.SerializeSize() + + // output to payee list + for _, payeeScript := range payeeScripts { + value := int64(1 + rand.Intn(100000000)) + txOut := wire.NewTxOut(value, payeeScript) + tx.AddTxOut(txOut) + //fmt.Printf("tx size: %d, output %d: %d\n", tx.SerializeSize(), i+1, tx.SerializeSize()-preTxSize) + preTxSize = tx.SerializeSize() + } + + // 3rd output to payee + value3 := int64(1 + rand.Intn(100000000)) + txOut3 := wire.NewTxOut(value3, payerScript) + tx.AddTxOut(txOut3) + require.Equal(t, bytesPerOutputP2WPKH, tx.SerializeSize()-preTxSize) + //fmt.Printf("tx size: %d, last output: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) +} + +func addTxInputsOutputsAndSignTx( + t *testing.T, tx *wire.MsgTx, + privateKey *btcec.PrivateKey, + payerScript []byte, + txids []string, + payeeScripts [][]byte) { + // Add inputs + addTxInputs(t, tx, txids) + + // Add outputs + addTxOutputs(t, tx, payerScript, payeeScripts) + + // Payer sign the redeeming transaction. + signTx(t, tx, payerScript, privateKey) +} + +func signTx(t *testing.T, tx *wire.MsgTx, payerScript []byte, privateKey *btcec.PrivateKey) { + preTxSize := tx.SerializeSize() + sigHashes := txscript.NewTxSigHashes(tx) + for ix := range tx.TxIn { + amount := int64(1 + rand.Intn(100000000)) + witnessHash, err := txscript.CalcWitnessSigHash(payerScript, sigHashes, txscript.SigHashAll, tx, ix, amount) + require.Nil(t, err) + sig, err := privateKey.Sign(witnessHash) + require.Nil(t, err) + + pkCompressed := privateKey.PubKey().SerializeCompressed() + txWitness := wire.TxWitness{append(sig.Serialize(), byte(txscript.SigHashAll)), pkCompressed} + tx.TxIn[ix].Witness = txWitness + + //fmt.Printf("tx size: %d, witness %d: %d\n", tx.SerializeSize(), ix+1, tx.SerializeSize()-preTxSize) + if ix == 0 { + bytesIncur := bytes1stWitness + len(tx.TxIn) - 1 // e.g., 130 bytes for a 21-input tx + require.True(t, tx.SerializeSize()-preTxSize >= bytesIncur-5) + require.True(t, tx.SerializeSize()-preTxSize <= bytesIncur+5) + } else { + require.True(t, tx.SerializeSize()-preTxSize >= bytesPerWitness-5) + require.True(t, tx.SerializeSize()-preTxSize <= bytesPerWitness+5) + } + preTxSize = tx.SerializeSize() + } +} + +func TestOutTxSize2In3Out(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + _, payee, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) + + // 2 example UTXO txids to use in the test. + utxosTxids := exampleTxids[:2] + + // Create a new transaction + tx := wire.NewMsgTx(wire.TxVersion) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, utxosTxids, [][]byte{payeeScript}) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(uint64(len(utxosTxids)), []btcutil.Address{payee}) + require.Equal(t, vBytes, vBytesEstimated) + require.Equal(t, vBytes, outTxBytesMin) +} + +func TestOutTxSize21In3Out(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + _, payee, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) + + // Create a new transaction + tx := wire.NewMsgTx(wire.TxVersion) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids, [][]byte{payeeScript}) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vError := uint64(21 / 4) // 5 vBytes error tolerance + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(uint64(len(exampleTxids)), []btcutil.Address{payee}) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + } +} + +func TestOutTxSizeXIn3Out(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + _, payee, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) + + // Create new transactions with X (2 <= X <= 21) inputs and 3 outputs respectively + for x := 2; x <= 21; x++ { + // Create transaction. Add inputs and outputs and sign the transaction + tx := wire.NewMsgTx(wire.TxVersion) + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:x], [][]byte{payeeScript}) + + // Estimate the tx size + // #nosec G701 always positive + vError := uint64(0.25 + float64(x)/4) // 1st witness incur 0.25 vByte error, other witness incur 1/4 vByte error tolerance, + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(uint64(len(exampleTxids[:x])), []btcutil.Address{payee}) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + //fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + //fmt.Printf("error percentage: %.2f%%\n", float64(vBytesEstimated-vBytes)/float64(vBytes)*100) + } + } +} + +func TestGetOutputSizeByAddress(t *testing.T) { + // test nil P2TR address and non-nil P2TR address + nilP2TR := (*bitcoin.AddressTaproot)(nil) + addrP2TR := getTestAddrScript(t, ScriptTypeP2TR) + require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2TR)) + require.Equal(t, uint64(bytesPerOutputP2TR), GetOutputSizeByAddress(addrP2TR)) + + // test nil P2WSH address and non-nil P2WSH address + nilP2WSH := (*btcutil.AddressWitnessScriptHash)(nil) + addrP2WSH := getTestAddrScript(t, ScriptTypeP2WSH) + require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2WSH)) + require.Equal(t, uint64(bytesPerOutputP2WSH), GetOutputSizeByAddress(addrP2WSH)) + + // test nil P2WPKH address and non-nil P2WPKH address + nilP2WPKH := (*btcutil.AddressWitnessPubKeyHash)(nil) + addrP2WPKH := getTestAddrScript(t, ScriptTypeP2WPKH) + require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2WPKH)) + require.Equal(t, uint64(bytesPerOutputP2WPKH), GetOutputSizeByAddress(addrP2WPKH)) + + // test nil P2SH address and non-nil P2SH address + nilP2SH := (*btcutil.AddressScriptHash)(nil) + addrP2SH := getTestAddrScript(t, ScriptTypeP2SH) + require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2SH)) + require.Equal(t, uint64(bytesPerOutputP2SH), GetOutputSizeByAddress(addrP2SH)) + + // test nil P2PKH address and non-nil P2PKH address + nilP2PKH := (*btcutil.AddressPubKeyHash)(nil) + addrP2PKH := getTestAddrScript(t, ScriptTypeP2PKH) + require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2PKH)) + require.Equal(t, uint64(bytesPerOutputP2PKH), GetOutputSizeByAddress(addrP2PKH)) +} + +func TestOutputSizeP2TR(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2TR) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(2, payees) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOutputSizeP2WSH(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2WSH) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(2, payees) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOutputSizeP2SH(t *testing.T) { + // Generate payer/payee private keys and P2SH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2SH) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(2, payees) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOutputSizeP2PKH(t *testing.T) { + // Generate payer/payee private keys and P2PKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2PKH) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(2, payees) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOuttxSizeBreakdown(t *testing.T) { + // a list of all types of addresses + payees := []btcutil.Address{ + getTestAddrScript(t, ScriptTypeP2TR), + getTestAddrScript(t, ScriptTypeP2WSH), + getTestAddrScript(t, ScriptTypeP2WPKH), + getTestAddrScript(t, ScriptTypeP2SH), + getTestAddrScript(t, ScriptTypeP2PKH), + } + + // add all outtx sizes paying to each address + txSizeTotal := uint64(0) + for _, payee := range payees { + txSizeTotal += EstimateOuttxSize(2, []btcutil.Address{payee}) + } + + // calculate the average outtx size + // #nosec G701 always in range + txSizeAverage := uint64((float64(txSizeTotal))/float64(len(payees)) + 0.5) + + // get deposit fee + txSizeDepositor := OuttxSizeDepositor() + require.Equal(t, uint64(68), txSizeDepositor) + + // get withdrawer fee + txSizeWithdrawer := OuttxSizeWithdrawer() + require.Equal(t, uint64(177), txSizeWithdrawer) + + // total outtx size == (deposit fee + withdrawer fee), 245 = 68 + 177 + require.Equal(t, outTxBytesAvg, txSizeAverage) + require.Equal(t, txSizeAverage, txSizeDepositor+txSizeWithdrawer) + + // check default depositor fee + depositFee := DepositorFee(defaultDepositorFeeRate) + require.Equal(t, depositFee, 0.00001360) +} + +func TestOuttxSizeMinMax(t *testing.T) { + // P2TR output is the largest in size; P2WPKH is the smallest + toP2TR := getTestAddrScript(t, ScriptTypeP2TR) + toP2WPKH := getTestAddrScript(t, ScriptTypeP2WPKH) + + // Estimate the largest outtx size in vByte + sizeMax := EstimateOuttxSize(21, []btcutil.Address{toP2TR}) + require.Equal(t, outTxBytesMax, sizeMax) + + // Estimate the smallest outtx size in vByte + sizeMin := EstimateOuttxSize(2, []btcutil.Address{toP2WPKH}) + require.Equal(t, outTxBytesMin, sizeMin) +} diff --git a/zetaclient/bitcoin/utils.go b/zetaclient/bitcoin/utils.go index aa816c9c66..fe2bb2481b 100644 --- a/zetaclient/bitcoin/utils.go +++ b/zetaclient/bitcoin/utils.go @@ -1,49 +1,13 @@ package bitcoin import ( - "encoding/hex" "encoding/json" - "fmt" "math" - "math/big" - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil" - "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/common" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - - "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" ) -const ( - bytesPerKB = 1000 - bytesEmptyTx = 10 // an empty tx is about 10 bytes - bytesPerInput = 41 // each input is about 41 bytes - bytesPerOutput = 31 // each output is about 31 bytes - bytes1stWitness = 110 // the 1st witness incurs about 110 bytes and it may vary - bytesPerWitness = 108 // each additional witness incurs about 108 bytes and it may vary - defaultDepositorFeeRate = 20 // 20 sat/byte is the default depositor fee rate -) - -var ( - BtcOutTxBytesDepositor uint64 - BtcOutTxBytesWithdrawer uint64 - DefaultDepositorFee float64 -) - -func init() { - BtcOutTxBytesDepositor = SegWitTxSizeDepositor() // 68vB, the outtx size incurred by the depositor - BtcOutTxBytesWithdrawer = SegWitTxSizeWithdrawer() // 171vB, the outtx size incurred by the withdrawer - - // default depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity. - // In reality, the fee rate on UTXO deposit is different from the fee rate when the UTXO is spent. - DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) // 0.00001360 (20 * 68vB / 100000000) -} - func PrettyPrintStruct(val interface{}) (string, error) { prettyStruct, err := json.MarshalIndent( val, @@ -56,143 +20,6 @@ func PrettyPrintStruct(val interface{}) (string, error) { return string(prettyStruct), nil } -// FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte. -func FeeRateToSatPerByte(rate float64) *big.Int { - // #nosec G701 always in range - satPerKB := new(big.Int).SetInt64(int64(rate * btcutil.SatoshiPerBitcoin)) - return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB)) -} - -// WiredTxSize calculates the wired tx size in bytes -func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 { - // Version 4 bytes + LockTime 4 bytes + Serialized varint size for the - // number of transaction inputs and outputs. - // #nosec G701 always positive - return uint64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs)) -} - -// EstimateSegWitTxSize estimates SegWit tx size -func EstimateSegWitTxSize(numInputs uint64, numOutputs uint64) uint64 { - if numInputs == 0 { - return 0 - } - bytesWiredTx := WiredTxSize(numInputs, numOutputs) - bytesInput := numInputs * bytesPerInput - bytesOutput := numOutputs * bytesPerOutput - bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness - // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations - // Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4 - return bytesWiredTx + bytesInput + bytesOutput + bytesWitness/blockchain.WitnessScaleFactor -} - -// SegWitTxSizeDepositor returns SegWit tx size (68vB) incurred by the depositor -func SegWitTxSizeDepositor() uint64 { - return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor -} - -// SegWitTxSizeWithdrawer returns SegWit tx size (171vB) incurred by the withdrawer (1 input, 3 outputs) -func SegWitTxSizeWithdrawer() uint64 { - bytesWiredTx := WiredTxSize(1, 3) - bytesInput := uint64(1) * bytesPerInput // nonce mark - bytesOutput := uint64(3) * bytesPerOutput // 3 outputs: new nonce mark, payment, change - return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor -} - -// DepositorFee calculates the depositor fee in BTC for a given sat/byte fee rate -// Note: the depositor fee is charged in order to cover the cost of spending the deposited UTXO in the future -func DepositorFee(satPerByte int64) float64 { - return float64(satPerByte) * float64(BtcOutTxBytesDepositor) / btcutil.SatoshiPerBitcoin -} - -// CalcBlockAvgFeeRate calculates the average gas rate (in sat/vByte) for a given block -func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *chaincfg.Params) (int64, error) { - // sanity check - if len(blockVb.Tx) == 0 { - return 0, errors.New("block has no transactions") - } - if len(blockVb.Tx) == 1 { - return 0, nil // only coinbase tx, it happens - } - txCoinbase := &blockVb.Tx[0] - if blockVb.Weight < blockchain.WitnessScaleFactor { - return 0, fmt.Errorf("block weight %d too small", blockVb.Weight) - } - if blockVb.Weight < txCoinbase.Weight { - return 0, fmt.Errorf("block weight %d less than coinbase tx weight %d", blockVb.Weight, txCoinbase.Weight) - } - if blockVb.Height <= 0 || blockVb.Height > math.MaxInt32 { - return 0, fmt.Errorf("invalid block height %d", blockVb.Height) - } - - // make sure the first tx is coinbase tx - txBytes, err := hex.DecodeString(txCoinbase.Hex) - if err != nil { - return 0, fmt.Errorf("failed to decode coinbase tx %s", txCoinbase.Txid) - } - tx, err := btcutil.NewTxFromBytes(txBytes) - if err != nil { - return 0, fmt.Errorf("failed to parse coinbase tx %s", txCoinbase.Txid) - } - if !blockchain.IsCoinBaseTx(tx.MsgTx()) { - return 0, fmt.Errorf("first tx %s is not coinbase tx", txCoinbase.Txid) - } - - // calculate fees earned by the miner - btcEarned := int64(0) - for _, out := range tx.MsgTx().TxOut { - if out.Value > 0 { - btcEarned += out.Value - } - } - // #nosec G701 checked above - subsidy := blockchain.CalcBlockSubsidy(int32(blockVb.Height), netParams) - if btcEarned < subsidy { - return 0, fmt.Errorf("miner earned %d, less than subsidy %d", btcEarned, subsidy) - } - txsFees := btcEarned - subsidy - - // sum up weight of all txs (<= 4 MWU) - txsWeight := int32(0) - for i, tx := range blockVb.Tx { - // coinbase doesn't pay fees, so we exclude it - if i > 0 && tx.Weight > 0 { - txsWeight += tx.Weight - } - } - - // calculate average fee rate. - vBytes := txsWeight / blockchain.WitnessScaleFactor - return txsFees / int64(vBytes), nil -} - -// CalcDepositorFee calculates the depositor fee for a given block -func CalcDepositorFee(blockVb *btcjson.GetBlockVerboseTxResult, chainID int64, netParams *chaincfg.Params, logger zerolog.Logger) float64 { - // use dynamic fee or default - dynamicFee := true - - // use default fee for regnet - if common.IsBitcoinRegnet(chainID) { - dynamicFee = false - } - // mainnet dynamic fee takes effect only after a planned upgrade height - if common.IsBitcoinMainnet(chainID) && blockVb.Height < DynamicDepositorFeeHeight { - dynamicFee = false - } - if !dynamicFee { - return DefaultDepositorFee - } - - // calculate deposit fee rate - feeRate, err := CalcBlockAvgFeeRate(blockVb, netParams) - if err != nil { - feeRate = defaultDepositorFeeRate // use default fee rate if calculation fails, should not happen - logger.Error().Err(err).Msgf("cannot calculate fee rate for block %d", blockVb.Height) - } - // #nosec G701 always in range - feeRate = int64(float64(feeRate) * clientcommon.BTCOuttxGasPriceMultiplier) - return DepositorFee(feeRate) -} - func GetSatoshis(btc float64) (int64, error) { // The amount is only considered invalid if it cannot be represented // as an integer type. This may happen if f is NaN or +-Infinity. diff --git a/zetaclient/evm/evm_signer_test.go b/zetaclient/evm/evm_signer_test.go index a17874ca02..c318a54de5 100644 --- a/zetaclient/evm/evm_signer_test.go +++ b/zetaclient/evm/evm_signer_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" corecommon "github.com/zeta-chain/zetacore/common" "github.com/zeta-chain/zetacore/testutil/sample" - "github.com/zeta-chain/zetacore/x/crosschain/types" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" "github.com/zeta-chain/zetacore/zetaclient/common" @@ -66,13 +65,13 @@ func getNewOutTxProcessor() *outtxprocessor.Processor { return outtxprocessor.NewOutTxProcessorManager(logger) } -func getCCTX() *types.CrossChainTx { +func getCCTX() *crosschaintypes.CrossChainTx { var cctx crosschaintypes.CrossChainTx testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) return &cctx } -func getInvalidCCTX() *types.CrossChainTx { +func getInvalidCCTX() *crosschaintypes.CrossChainTx { var cctx crosschaintypes.CrossChainTx testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) return &cctx From c338d41920b0ef1472331ec3e204b8fdebf7eb7f Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 26 Mar 2024 00:51:12 -0500 Subject: [PATCH 09/16] added changelog entry --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 255acc2fb7..2ee9241d08 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,7 @@ * [1755](https://github.com/zeta-chain/node/issues/1755) - use evm JSON RPC for inbound tx (including blob tx) observation. * [1815](https://github.com/zeta-chain/node/pull/1815) - add authority module for authorized actions * [1884](https://github.com/zeta-chain/node/pull/1884) - added zetatool cmd, added subcommand to filter deposits +* [1942](https://github.com/zeta-chain/node/pull/1942) - support Bitcoin P2TR, P2WSH, P2SH, P2PKH addresses ### Tests From 31a0dfecce3387f43d699f18f4d51c74790ac3d4 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 26 Mar 2024 12:14:15 -0500 Subject: [PATCH 10/16] fix unit tests --- x/crosschain/keeper/evm_hooks_test.go | 21 --------------------- zetaclient/bitcoin/fee_test.go | 10 +++++++--- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index 344957a117..72b44d5804 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -715,27 +715,6 @@ func TestKeeper_ProcessLogs(t *testing.T) { require.Len(t, cctxList, 0) }) - t.Run("error returned for invalid event data", func(t *testing.T) { - k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) - k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) - - chain := common.BtcMainnetChain() - chainID := chain.ChainId - setSupportedChain(ctx, zk, chainID) - SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - - block := sample.GetInvalidZRC20WithdrawToExternal(t) - gasZRC20 := setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, chainID, "bitcoin", "BTC") - for _, log := range block.Logs { - log.Address = gasZRC20 - } - - err := k.ProcessLogs(ctx, block.Logs, sample.EthAddress(), "") - require.ErrorContains(t, err, "ParseZRC20WithdrawalEvent: invalid address") - cctxList := k.GetAllCrossChainTx(ctx) - require.Len(t, cctxList, 0) - }) - t.Run("error returned if unable to process an event", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) diff --git a/zetaclient/bitcoin/fee_test.go b/zetaclient/bitcoin/fee_test.go index fb14f08c2d..5ed7a6c54e 100644 --- a/zetaclient/bitcoin/fee_test.go +++ b/zetaclient/bitcoin/fee_test.go @@ -197,10 +197,14 @@ func TestOutTxSize2In3Out(t *testing.T) { // Estimate the tx size in vByte // #nosec G701 always positive + vError := uint64(1) // 1 vByte error tolerance vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) vBytesEstimated := EstimateOuttxSize(uint64(len(utxosTxids)), []btcutil.Address{payee}) - require.Equal(t, vBytes, vBytesEstimated) - require.Equal(t, vBytes, outTxBytesMin) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + } } func TestOutTxSize21In3Out(t *testing.T) { @@ -239,7 +243,7 @@ func TestOutTxSizeXIn3Out(t *testing.T) { // Estimate the tx size // #nosec G701 always positive - vError := uint64(0.25 + float64(x)/4) // 1st witness incur 0.25 vByte error, other witness incur 1/4 vByte error tolerance, + vError := uint64(0.25 + float64(x)/4) // 1st witness incurs 0.25 more vByte error than others (which incurs 1/4 vByte per witness) vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) vBytesEstimated := EstimateOuttxSize(uint64(len(exampleTxids[:x])), []btcutil.Address{payee}) if vBytes > vBytesEstimated { From 82527b5759aba15f869c43d418f559fc9067a602 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 26 Mar 2024 16:37:17 -0500 Subject: [PATCH 11/16] added e2e tests for different types of bitcoin addresses --- cmd/zetae2e/local/local.go | 6 +- e2e/e2etests/e2etests.go | 53 +++++++- e2e/e2etests/test_bitcoin_withdraw.go | 126 ++++++++++++++++-- e2e/e2etests/test_bitcoin_withdraw_invalid.go | 1 + 4 files changed, 166 insertions(+), 20 deletions(-) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 6aef91156a..fe1e3faff7 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -250,7 +250,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestZetaDepositRestrictedName, } bitcoinTests := []string{ - e2etests.TestBitcoinWithdrawName, + e2etests.TestBitcoinWithdrawSegWitName, + e2etests.TestBitcoinWithdrawTaprootName, + e2etests.TestBitcoinWithdrawLegacyName, + e2etests.TestBitcoinWithdrawP2SHName, + e2etests.TestBitcoinWithdrawP2WSHName, e2etests.TestBitcoinWithdrawInvalidAddressName, e2etests.TestZetaWithdrawBTCRevertName, e2etests.TestCrosschainSwapName, diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 70bdc6de94..b5b598ed01 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -15,7 +15,11 @@ const ( TestZetaWithdrawBTCRevertName = "zeta_withdraw_btc_revert" // #nosec G101 - not a hardcoded password TestMessagePassingName = "message_passing" TestZRC20SwapName = "zrc20_swap" - TestBitcoinWithdrawName = "bitcoin_withdraw" + TestBitcoinWithdrawSegWitName = "bitcoin_withdraw_segwit" + TestBitcoinWithdrawTaprootName = "bitcoin_withdraw_taproot" + TestBitcoinWithdrawLegacyName = "bitcoin_withdraw_legacy" + TestBitcoinWithdrawP2WSHName = "bitcoin_withdraw_p2wsh" + TestBitcoinWithdrawP2SHName = "bitcoin_withdraw_p2sh" TestBitcoinWithdrawInvalidAddressName = "bitcoin_withdraw_invalid" TestBitcoinWithdrawRestrictedName = "bitcoin_withdraw_restricted" TestCrosschainSwapName = "crosschain_swap" @@ -130,12 +134,49 @@ var AllE2ETests = []runner.E2ETest{ TestZRC20Swap, ), runner.NewE2ETest( - TestBitcoinWithdrawName, - "withdraw BTC from ZEVM", + TestBitcoinWithdrawSegWitName, + "withdraw BTC from ZEVM to a SegWit address", []runner.ArgDefinition{ - runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.01"}, + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawSegWit, + ), + runner.NewE2ETest( + TestBitcoinWithdrawTaprootName, + "withdraw BTC from ZEVM to a Taproot address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawTaproot, + ), + runner.NewE2ETest( + TestBitcoinWithdrawLegacyName, + "withdraw BTC from ZEVM to a legacy address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, }, - TestBitcoinWithdraw, + TestBitcoinWithdrawLegacy, + ), + runner.NewE2ETest( + TestBitcoinWithdrawP2WSHName, + "withdraw BTC from ZEVM to a P2WSH address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawP2WSH, + ), + runner.NewE2ETest( + TestBitcoinWithdrawP2SHName, + "withdraw BTC from ZEVM to a P2SH address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawP2SH, ), runner.NewE2ETest( TestBitcoinWithdrawInvalidAddressName, @@ -311,7 +352,7 @@ var AllE2ETests = []runner.E2ETest{ TestBitcoinWithdrawRestrictedName, "withdraw Bitcoin from ZEVM to restricted address", []runner.ArgDefinition{ - runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.01"}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, }, TestBitcoinWithdrawRestricted, ), diff --git a/e2e/e2etests/test_bitcoin_withdraw.go b/e2e/e2etests/test_bitcoin_withdraw.go index edd779b595..60871699b4 100644 --- a/e2e/e2etests/test_bitcoin_withdraw.go +++ b/e2e/e2etests/test_bitcoin_withdraw.go @@ -9,30 +9,100 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/common/bitcoin" "github.com/zeta-chain/zetacore/e2e/runner" "github.com/zeta-chain/zetacore/e2e/utils" "github.com/zeta-chain/zetacore/zetaclient/testutils" ) -func TestBitcoinWithdraw(r *runner.E2ERunner, args []string) { - if len(args) != 1 { - panic("TestBitcoinWithdraw requires exactly one argument for the amount.") +func TestBitcoinWithdrawSegWit(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawSegWit requires two arguments: [receiver, amount]") } + r.SetBtcAddress(r.Name, false) - withdrawalAmount, err := strconv.ParseFloat(args[0], 64) - if err != nil { - panic("Invalid withdrawal amount specified for TestBitcoinWithdraw.") + // parse arguments + defaultReceiver := r.BTCDeployerAddress.EncodeAddress() + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressWitnessPubKeyHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawSegWit.") } - withdrawalAmountSat, err := btcutil.NewAmount(withdrawalAmount) - if err != nil { - panic(err) + WithdrawBitcoin(r, receiver, amount) +} + +func TestBitcoinWithdrawTaproot(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawTaproot requires two arguments: [receiver, amount]") } - amount := big.NewInt(int64(withdrawalAmountSat)) + r.SetBtcAddress(r.Name, false) + // parse arguments and withdraw BTC + defaultReceiver := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*bitcoin.AddressTaproot) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawTaproot.") + } + + WithdrawBitcoin(r, receiver, amount) +} + +func TestBitcoinWithdrawLegacy(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawLegacy requires two arguments: [receiver, amount]") + } r.SetBtcAddress(r.Name, false) - WithdrawBitcoin(r, amount) + // parse arguments and withdraw BTC + defaultReceiver := "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressPubKeyHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawLegacy.") + } + + WithdrawBitcoin(r, receiver, amount) +} + +func TestBitcoinWithdrawP2WSH(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawP2WSH requires two arguments: [receiver, amount]") + } + r.SetBtcAddress(r.Name, false) + + // parse arguments and withdraw BTC + defaultReceiver := "bcrt1qm9mzhyky4w853ft2ms6dtqdyyu3z2tmrq8jg8xglhyuv0dsxzmgs2f0sqy" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressWitnessScriptHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawP2WSH.") + } + + WithdrawBitcoin(r, receiver, amount) +} + +func TestBitcoinWithdrawP2SH(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawP2SH requires two arguments: [receiver, amount]") + } + r.SetBtcAddress(r.Name, false) + + // parse arguments and withdraw BTC + defaultReceiver := "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressScriptHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawP2SH.") + } + + WithdrawBitcoin(r, receiver, amount) } func TestBitcoinWithdrawRestricted(r *runner.E2ERunner, args []string) { @@ -56,6 +126,36 @@ func TestBitcoinWithdrawRestricted(r *runner.E2ERunner, args []string) { WithdrawBitcoinRestricted(r, amount) } +func parseBitcoinWithdrawArgs(args []string, defaultReceiver string) (btcutil.Address, *big.Int) { + // parse receiver address + var err error + var receiver btcutil.Address + if args[0] == "" { + // use the default receiver + receiver, err = common.DecodeBtcAddress(defaultReceiver, common.BtcRegtestChain().ChainId) + if err != nil { + panic("Invalid default receiver address specified for TestBitcoinWithdraw.") + } + } else { + receiver, err = common.DecodeBtcAddress(args[0], common.BtcRegtestChain().ChainId) + if err != nil { + panic("Invalid receiver address specified for TestBitcoinWithdraw.") + } + } + // parse the withdrawal amount + withdrawalAmount, err := strconv.ParseFloat(args[1], 64) + if err != nil { + panic("Invalid withdrawal amount specified for TestBitcoinWithdraw.") + } + withdrawalAmountSat, err := btcutil.NewAmount(withdrawalAmount) + if err != nil { + panic(err) + } + amount := big.NewInt(int64(withdrawalAmountSat)) + + return receiver, amount +} + func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) *btcjson.TxRawResult { tx, err := r.BTCZRC20.Approve(r.ZEVMAuth, r.BTCZRC20Addr, big.NewInt(amount.Int64()*2)) // approve more to cover withdraw fee if err != nil { @@ -116,8 +216,8 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) return rawTx } -func WithdrawBitcoin(r *runner.E2ERunner, amount *big.Int) { - withdrawBTCZRC20(r, r.BTCDeployerAddress, amount) +func WithdrawBitcoin(r *runner.E2ERunner, receiver btcutil.Address, amount *big.Int) { + withdrawBTCZRC20(r, receiver, amount) } func WithdrawBitcoinRestricted(r *runner.E2ERunner, amount *big.Int) { diff --git a/e2e/e2etests/test_bitcoin_withdraw_invalid.go b/e2e/e2etests/test_bitcoin_withdraw_invalid.go index 311895f568..88c1df95c1 100644 --- a/e2e/e2etests/test_bitcoin_withdraw_invalid.go +++ b/e2e/e2etests/test_bitcoin_withdraw_invalid.go @@ -48,6 +48,7 @@ func WithdrawToInvalidAddress(r *runner.E2ERunner, amount *big.Int) { stop := r.MineBlocks() // withdraw amount provided as test arg BTC from ZRC20 to BTC legacy address + // the address "1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3" is for mainnet, not regtest tx, err = r.BTCZRC20.Withdraw(r.ZEVMAuth, []byte("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3"), amount) if err != nil { panic(err) From d2935b9b911c39d487d907d746288e7c62d8838e Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 27 Mar 2024 15:20:44 +0100 Subject: [PATCH 12/16] try retriggering CI --- changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 2ee9241d08..3c0a64c09e 100644 --- a/changelog.md +++ b/changelog.md @@ -160,6 +160,7 @@ * [1675](https://github.com/zeta-chain/node/issues/1675) - use chain param ConfirmationCount for bitcoin confirmation ## Chores + * [1694](https://github.com/zeta-chain/node/pull/1694) - remove standalone network, use require testing package for the entire node folder ## Version: v12.1.0 @@ -172,6 +173,7 @@ * [1658](https://github.com/zeta-chain/node/pull/1658) - modify emission distribution to use fixed block rewards ### Fixes + * [1535](https://github.com/zeta-chain/node/issues/1535) - Avoid voting on wrong ballots due to false blockNumber in EVM tx receipt * [1588](https://github.com/zeta-chain/node/pull/1588) - fix chain params comparison logic * [1650](https://github.com/zeta-chain/node/pull/1605) - exempt (discounted) *system txs* from min gas price check and gas fee deduction From bc02fb7111f545813f3e5dca7ab34367d2013c1b Mon Sep 17 00:00:00 2001 From: lumtis Date: Wed, 27 Mar 2024 15:22:03 +0100 Subject: [PATCH 13/16] e2e tests all PR --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c273f4633d..9bbfd4c4b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: - develop pull_request: branches: - - develop + - "*" types: - synchronize - opened From 2bcdfbc727c12612159ffbf008bb00505a996b4b Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 28 Mar 2024 15:21:50 -0500 Subject: [PATCH 14/16] resolved some PR review feedback --- e2e/e2etests/test_bitcoin_withdraw.go | 25 +++--- x/crosschain/keeper/evm_hooks_test.go | 22 +++++ zetaclient/bitcoin/bitcoin_client_test.go | 32 +++---- zetaclient/bitcoin/bitcoin_signer.go | 10 ++- zetaclient/bitcoin/bitcoin_signer_test.go | 14 +++ zetaclient/bitcoin/fee.go | 67 +++++++------- zetaclient/bitcoin/fee_test.go | 90 ++++++++++++++----- .../bitcoin/{txscript.go => tx_script.go} | 26 +++--- .../{txscript_test.go => tx_script_test.go} | 0 9 files changed, 187 insertions(+), 99 deletions(-) rename zetaclient/bitcoin/{txscript.go => tx_script.go} (94%) rename zetaclient/bitcoin/{txscript_test.go => tx_script_test.go} (100%) diff --git a/e2e/e2etests/test_bitcoin_withdraw.go b/e2e/e2etests/test_bitcoin_withdraw.go index 60871699b4..d74ee43615 100644 --- a/e2e/e2etests/test_bitcoin_withdraw.go +++ b/e2e/e2etests/test_bitcoin_withdraw.go @@ -12,6 +12,7 @@ import ( "github.com/zeta-chain/zetacore/common/bitcoin" "github.com/zeta-chain/zetacore/e2e/runner" "github.com/zeta-chain/zetacore/e2e/utils" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/testutils" ) @@ -30,7 +31,7 @@ func TestBitcoinWithdrawSegWit(r *runner.E2ERunner, args []string) { panic("Invalid receiver address specified for TestBitcoinWithdrawSegWit.") } - WithdrawBitcoin(r, receiver, amount) + withdrawBTCZRC20(r, receiver, amount) } func TestBitcoinWithdrawTaproot(r *runner.E2ERunner, args []string) { @@ -48,7 +49,7 @@ func TestBitcoinWithdrawTaproot(r *runner.E2ERunner, args []string) { panic("Invalid receiver address specified for TestBitcoinWithdrawTaproot.") } - WithdrawBitcoin(r, receiver, amount) + withdrawBTCZRC20(r, receiver, amount) } func TestBitcoinWithdrawLegacy(r *runner.E2ERunner, args []string) { @@ -66,7 +67,7 @@ func TestBitcoinWithdrawLegacy(r *runner.E2ERunner, args []string) { panic("Invalid receiver address specified for TestBitcoinWithdrawLegacy.") } - WithdrawBitcoin(r, receiver, amount) + withdrawBTCZRC20(r, receiver, amount) } func TestBitcoinWithdrawP2WSH(r *runner.E2ERunner, args []string) { @@ -84,7 +85,7 @@ func TestBitcoinWithdrawP2WSH(r *runner.E2ERunner, args []string) { panic("Invalid receiver address specified for TestBitcoinWithdrawP2WSH.") } - WithdrawBitcoin(r, receiver, amount) + withdrawBTCZRC20(r, receiver, amount) } func TestBitcoinWithdrawP2SH(r *runner.E2ERunner, args []string) { @@ -102,7 +103,7 @@ func TestBitcoinWithdrawP2SH(r *runner.E2ERunner, args []string) { panic("Invalid receiver address specified for TestBitcoinWithdrawP2SH.") } - WithdrawBitcoin(r, receiver, amount) + withdrawBTCZRC20(r, receiver, amount) } func TestBitcoinWithdrawRestricted(r *runner.E2ERunner, args []string) { @@ -123,7 +124,7 @@ func TestBitcoinWithdrawRestricted(r *runner.E2ERunner, args []string) { r.SetBtcAddress(r.Name, false) - WithdrawBitcoinRestricted(r, amount) + withdrawBitcoinRestricted(r, amount) } func parseBitcoinWithdrawArgs(args []string, defaultReceiver string) (btcutil.Address, *big.Int) { @@ -185,7 +186,13 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) panic(err) } + // get cctx and check status cctx := utils.WaitCctxMinedByInTxHash(r.Ctx, receipt.TxHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_OutboundMined { + panic(fmt.Errorf("cctx status is not OutboundMined")) + } + + // get bitcoin tx according to the outTxHash in cctx outTxHash := cctx.GetCurrentOutTxParam().OutboundTxHash hash, err := chainhash.NewHashFromStr(outTxHash) if err != nil { @@ -216,11 +223,7 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) return rawTx } -func WithdrawBitcoin(r *runner.E2ERunner, receiver btcutil.Address, amount *big.Int) { - withdrawBTCZRC20(r, receiver, amount) -} - -func WithdrawBitcoinRestricted(r *runner.E2ERunner, amount *big.Int) { +func withdrawBitcoinRestricted(r *runner.E2ERunner, amount *big.Int) { // use restricted BTC P2WPKH address addressRestricted, err := common.DecodeBtcAddress(testutils.RestrictedBtcAddressTest, common.BtcRegtestChain().ChainId) if err != nil { diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index 72b44d5804..26755b4872 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -715,6 +715,28 @@ func TestKeeper_ProcessLogs(t *testing.T) { require.Len(t, cctxList, 0) }) + t.Run("error returned for invalid event data", func(t *testing.T) { + k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + + // use the wrong (testnet) chain ID to make the btc address parsing fail + chain := common.BtcTestNetChain() + chainID := chain.ChainId + setSupportedChain(ctx, zk, chainID) + SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) + + block := sample.GetInvalidZRC20WithdrawToExternal(t) + gasZRC20 := setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, chainID, "bitcoin", "BTC") + for _, log := range block.Logs { + log.Address = gasZRC20 + } + + err := k.ProcessLogs(ctx, block.Logs, sample.EthAddress(), "") + require.ErrorContains(t, err, "ParseZRC20WithdrawalEvent: invalid address") + cctxList := k.GetAllCrossChainTx(ctx) + require.Len(t, cctxList, 0) + }) + t.Run("error returned if unable to process an event", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index b1fc32b8c7..1cf91c910f 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -41,6 +41,22 @@ func MockBTCClientMainnet() *BTCChainClient { } } +// createRPCClientAndLoadTx is a helper function to load raw tx and feed it to mock rpc client +func createRPCClientAndLoadTx(chainId int64, txHash string) *stub.MockBTCRPCClient { + // file name for the archived MsgTx + nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainId, txHash)) + + // load archived MsgTx + var msgTx wire.MsgTx + testutils.LoadObjectFromJSONFile(&msgTx, nameMsgTx) + tx := btcutil.NewTx(&msgTx) + + // feed tx to mock rpc client + rpcClient := stub.NewMockBTCRPCClient() + rpcClient.WithRawTransaction(tx) + return rpcClient +} + func TestNewBitcoinClient(t *testing.T) { t.Run("should return error because zetacore doesn't update core context", func(t *testing.T) { cfg := config.NewConfig() @@ -357,22 +373,6 @@ func TestCheckTSSVoutCancelled(t *testing.T) { }) } -// createRPCClientAndLoadTx is a helper function to load raw tx and feed it to mock rpc client -func createRPCClientAndLoadTx(chainId int64, txHash string) *stub.MockBTCRPCClient { - // file name for the archived MsgTx - nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainId, txHash)) - - // load archived MsgTx - var msgTx wire.MsgTx - testutils.LoadObjectFromJSONFile(&msgTx, nameMsgTx) - tx := btcutil.NewTx(&msgTx) - - // feed tx to mock rpc client - rpcClient := stub.NewMockBTCRPCClient() - rpcClient.WithRawTransaction(tx) - return rpcClient -} - func TestGetSenderAddressByVin(t *testing.T) { chain := common.BtcMainnetChain() net := &chaincfg.MainNetParams diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index 83ef32e7a4..31e2993c56 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -98,7 +98,10 @@ func (signer *BTCSigner) GetERC20CustodyAddress() ethcommon.Address { return ethcommon.Address{} } -// AddWithdrawTxOutputs adds the outputs to the withdraw tx +// AddWithdrawTxOutputs adds the 3 outputs to the withdraw tx +// 1st output: the nonce-mark btc to TSS itself +// 2nd output: the payment to the recipient +// 3rd output: the remaining btc to TSS itself func (signer *BTCSigner) AddWithdrawTxOutputs( tx *wire.MsgTx, to btcutil.Address, @@ -200,7 +203,10 @@ func (signer *BTCSigner) SignWithdrawTx( // size checking // #nosec G701 always positive - txSize := EstimateOuttxSize(uint64(len(prevOuts)), []btcutil.Address{to}) + txSize, err := EstimateOuttxSize(uint64(len(prevOuts)), []btcutil.Address{to}) + if err != nil { + return nil, err + } if sizeLimit < BtcOutTxBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user signer.logger.Info().Msgf("sizeLimit %d is less than BtcOutTxBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce) } diff --git a/zetaclient/bitcoin/bitcoin_signer_test.go b/zetaclient/bitcoin/bitcoin_signer_test.go index 575b8f0d09..3c72b32662 100644 --- a/zetaclient/bitcoin/bitcoin_signer_test.go +++ b/zetaclient/bitcoin/bitcoin_signer_test.go @@ -292,6 +292,20 @@ func TestAddWithdrawTxOutputs(t *testing.T) { {Value: 80000000, PkScript: tssScript}, }, }, + { + name: "should add outputs without change successfully", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.20012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 20000000, PkScript: toScript}, + }, + }, { name: "should cancel tx successfully", tx: wire.NewMsgTx(wire.TxVersion), diff --git a/zetaclient/bitcoin/fee.go b/zetaclient/bitcoin/fee.go index 59017448ce..022b5ae993 100644 --- a/zetaclient/bitcoin/fee.go +++ b/zetaclient/bitcoin/fee.go @@ -9,19 +9,17 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/zeta-chain/zetacore/common" "github.com/zeta-chain/zetacore/common/bitcoin" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - - "github.com/btcsuite/btcd/wire" - "github.com/pkg/errors" ) const ( bytesPerKB = 1000 - bytesEmptyTx = 10 // an empty tx is 10 bytes bytesPerInput = 41 // each input is 41 bytes bytesPerOutputP2TR = 43 // each P2TR output is 43 bytes bytesPerOutputP2WSH = 43 // each P2WSH output is 43 bytes @@ -39,19 +37,16 @@ const ( ) var ( - BtcOutTxBytesDepositor uint64 - BtcOutTxBytesWithdrawer uint64 - DefaultDepositorFee float64 -) + // The outtx size incurred by the depositor: 68vB + BtcOutTxBytesDepositor = OuttxSizeDepositor() -func init() { - BtcOutTxBytesDepositor = OuttxSizeDepositor() // 68vB, the outtx size incurred by the depositor - BtcOutTxBytesWithdrawer = OuttxSizeWithdrawer() // 177vB, the outtx size incurred by the withdrawer + // The outtx size incurred by the withdrawer: 177vB + BtcOutTxBytesWithdrawer = OuttxSizeWithdrawer() + // The default depositor fee is 0.00001360 BTC (20 * 68vB / 100000000) // default depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity. - // In reality, the fee rate on UTXO deposit is different from the fee rate when the UTXO is spent. - DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) // 0.00001360 (20 * 68vB / 100000000) -} + DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) +) // FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte. func FeeRateToSatPerByte(rate float64) *big.Int { @@ -69,9 +64,9 @@ func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 { } // EstimateOuttxSize estimates the size of a outtx in vBytes -func EstimateOuttxSize(numInputs uint64, payees []btcutil.Address) uint64 { +func EstimateOuttxSize(numInputs uint64, payees []btcutil.Address) (uint64, error) { if numInputs == 0 { - return 0 + return 0, nil } // #nosec G701 always positive numOutputs := 2 + uint64(len(payees)) @@ -82,45 +77,49 @@ func EstimateOuttxSize(numInputs uint64, payees []btcutil.Address) uint64 { // calculate the size of the outputs to payees bytesToPayees := uint64(0) for _, to := range payees { - bytesToPayees += GetOutputSizeByAddress(to) + sizeOutput, err := GetOutputSizeByAddress(to) + if err != nil { + return 0, err + } + bytesToPayees += sizeOutput } // calculate the size of the witness bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations // Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4 - return bytesWiredTx + bytesInput + bytesOutput + bytesToPayees + bytesWitness/blockchain.WitnessScaleFactor + return bytesWiredTx + bytesInput + bytesOutput + bytesToPayees + bytesWitness/blockchain.WitnessScaleFactor, nil } // GetOutputSizeByAddress returns the size of a tx output in bytes by the given address -func GetOutputSizeByAddress(to btcutil.Address) uint64 { +func GetOutputSizeByAddress(to btcutil.Address) (uint64, error) { switch addr := to.(type) { case *bitcoin.AddressTaproot: if addr == nil { - return 0 + return 0, nil } - return bytesPerOutputP2TR + return bytesPerOutputP2TR, nil case *btcutil.AddressWitnessScriptHash: if addr == nil { - return 0 + return 0, nil } - return bytesPerOutputP2WSH + return bytesPerOutputP2WSH, nil case *btcutil.AddressWitnessPubKeyHash: if addr == nil { - return 0 + return 0, nil } - return bytesPerOutputP2WPKH + return bytesPerOutputP2WPKH, nil case *btcutil.AddressScriptHash: if addr == nil { - return 0 + return 0, nil } - return bytesPerOutputP2SH + return bytesPerOutputP2SH, nil case *btcutil.AddressPubKeyHash: if addr == nil { - return 0 + return 0, nil } - return bytesPerOutputP2PKH + return bytesPerOutputP2PKH, nil default: - return bytesPerOutputP2WPKH + return 0, fmt.Errorf("cannot get output size for address type %T", to) } } @@ -207,18 +206,12 @@ func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *ch // CalcDepositorFee calculates the depositor fee for a given block func CalcDepositorFee(blockVb *btcjson.GetBlockVerboseTxResult, chainID int64, netParams *chaincfg.Params, logger zerolog.Logger) float64 { - // use dynamic fee or default - dynamicFee := true - // use default fee for regnet if common.IsBitcoinRegnet(chainID) { - dynamicFee = false + return DefaultDepositorFee } // mainnet dynamic fee takes effect only after a planned upgrade height if common.IsBitcoinMainnet(chainID) && blockVb.Height < DynamicDepositorFeeHeight { - dynamicFee = false - } - if !dynamicFee { return DefaultDepositorFee } diff --git a/zetaclient/bitcoin/fee_test.go b/zetaclient/bitcoin/fee_test.go index 5ed7a6c54e..bf42f32375 100644 --- a/zetaclient/bitcoin/fee_test.go +++ b/zetaclient/bitcoin/fee_test.go @@ -199,7 +199,8 @@ func TestOutTxSize2In3Out(t *testing.T) { // #nosec G701 always positive vError := uint64(1) // 1 vByte error tolerance vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(uint64(len(utxosTxids)), []btcutil.Address{payee}) + vBytesEstimated, err := EstimateOuttxSize(uint64(len(utxosTxids)), []btcutil.Address{payee}) + require.NoError(t, err) if vBytes > vBytesEstimated { require.True(t, vBytes-vBytesEstimated <= vError) } else { @@ -222,7 +223,8 @@ func TestOutTxSize21In3Out(t *testing.T) { // #nosec G701 always positive vError := uint64(21 / 4) // 5 vBytes error tolerance vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(uint64(len(exampleTxids)), []btcutil.Address{payee}) + vBytesEstimated, err := EstimateOuttxSize(uint64(len(exampleTxids)), []btcutil.Address{payee}) + require.NoError(t, err) if vBytes > vBytesEstimated { require.True(t, vBytes-vBytesEstimated <= vError) } else { @@ -245,7 +247,8 @@ func TestOutTxSizeXIn3Out(t *testing.T) { // #nosec G701 always positive vError := uint64(0.25 + float64(x)/4) // 1st witness incurs 0.25 more vByte error than others (which incurs 1/4 vByte per witness) vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(uint64(len(exampleTxids[:x])), []btcutil.Address{payee}) + vBytesEstimated, err := EstimateOuttxSize(uint64(len(exampleTxids[:x])), []btcutil.Address{payee}) + require.NoError(t, err) if vBytes > vBytesEstimated { require.True(t, vBytes-vBytesEstimated <= vError) //fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100) @@ -259,33 +262,64 @@ func TestOutTxSizeXIn3Out(t *testing.T) { func TestGetOutputSizeByAddress(t *testing.T) { // test nil P2TR address and non-nil P2TR address nilP2TR := (*bitcoin.AddressTaproot)(nil) + sizeNilP2TR, err := GetOutputSizeByAddress(nilP2TR) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2TR) + addrP2TR := getTestAddrScript(t, ScriptTypeP2TR) - require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2TR)) - require.Equal(t, uint64(bytesPerOutputP2TR), GetOutputSizeByAddress(addrP2TR)) + sizeP2TR, err := GetOutputSizeByAddress(addrP2TR) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2TR), sizeP2TR) // test nil P2WSH address and non-nil P2WSH address nilP2WSH := (*btcutil.AddressWitnessScriptHash)(nil) + sizeNilP2WSH, err := GetOutputSizeByAddress(nilP2WSH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2WSH) + addrP2WSH := getTestAddrScript(t, ScriptTypeP2WSH) - require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2WSH)) - require.Equal(t, uint64(bytesPerOutputP2WSH), GetOutputSizeByAddress(addrP2WSH)) + sizeP2WSH, err := GetOutputSizeByAddress(addrP2WSH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2WSH), sizeP2WSH) // test nil P2WPKH address and non-nil P2WPKH address nilP2WPKH := (*btcutil.AddressWitnessPubKeyHash)(nil) + sizeNilP2WPKH, err := GetOutputSizeByAddress(nilP2WPKH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2WPKH) + addrP2WPKH := getTestAddrScript(t, ScriptTypeP2WPKH) - require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2WPKH)) - require.Equal(t, uint64(bytesPerOutputP2WPKH), GetOutputSizeByAddress(addrP2WPKH)) + sizeP2WPKH, err := GetOutputSizeByAddress(addrP2WPKH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2WPKH), sizeP2WPKH) // test nil P2SH address and non-nil P2SH address nilP2SH := (*btcutil.AddressScriptHash)(nil) + sizeNilP2SH, err := GetOutputSizeByAddress(nilP2SH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2SH) + addrP2SH := getTestAddrScript(t, ScriptTypeP2SH) - require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2SH)) - require.Equal(t, uint64(bytesPerOutputP2SH), GetOutputSizeByAddress(addrP2SH)) + sizeP2SH, err := GetOutputSizeByAddress(addrP2SH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2SH), sizeP2SH) // test nil P2PKH address and non-nil P2PKH address nilP2PKH := (*btcutil.AddressPubKeyHash)(nil) + sizeNilP2PKH, err := GetOutputSizeByAddress(nilP2PKH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2PKH) + addrP2PKH := getTestAddrScript(t, ScriptTypeP2PKH) - require.Equal(t, uint64(0), GetOutputSizeByAddress(nilP2PKH)) - require.Equal(t, uint64(bytesPerOutputP2PKH), GetOutputSizeByAddress(addrP2PKH)) + sizeP2PKH, err := GetOutputSizeByAddress(addrP2PKH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2PKH), sizeP2PKH) + + // test unsupported address type + nilP2PK := (*btcutil.AddressPubKey)(nil) + sizeP2PK, err := GetOutputSizeByAddress(nilP2PK) + require.ErrorContains(t, err, "cannot get output size for address type") + require.Equal(t, uint64(0), sizeP2PK) } func TestOutputSizeP2TR(t *testing.T) { @@ -303,7 +337,8 @@ func TestOutputSizeP2TR(t *testing.T) { // Estimate the tx size in vByte // #nosec G701 always positive vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(2, payees) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) require.Equal(t, vBytes, vBytesEstimated) } @@ -322,7 +357,8 @@ func TestOutputSizeP2WSH(t *testing.T) { // Estimate the tx size in vByte // #nosec G701 always positive vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(2, payees) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) require.Equal(t, vBytes, vBytesEstimated) } @@ -341,7 +377,8 @@ func TestOutputSizeP2SH(t *testing.T) { // Estimate the tx size in vByte // #nosec G701 always positive vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(2, payees) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) require.Equal(t, vBytes, vBytesEstimated) } @@ -360,7 +397,8 @@ func TestOutputSizeP2PKH(t *testing.T) { // Estimate the tx size in vByte // #nosec G701 always positive vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateOuttxSize(2, payees) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) require.Equal(t, vBytes, vBytesEstimated) } @@ -377,7 +415,9 @@ func TestOuttxSizeBreakdown(t *testing.T) { // add all outtx sizes paying to each address txSizeTotal := uint64(0) for _, payee := range payees { - txSizeTotal += EstimateOuttxSize(2, []btcutil.Address{payee}) + sizeOutput, err := EstimateOuttxSize(2, []btcutil.Address{payee}) + require.NoError(t, err) + txSizeTotal += sizeOutput } // calculate the average outtx size @@ -401,16 +441,24 @@ func TestOuttxSizeBreakdown(t *testing.T) { require.Equal(t, depositFee, 0.00001360) } -func TestOuttxSizeMinMax(t *testing.T) { +func TestOuttxSizeMinMaxError(t *testing.T) { // P2TR output is the largest in size; P2WPKH is the smallest toP2TR := getTestAddrScript(t, ScriptTypeP2TR) toP2WPKH := getTestAddrScript(t, ScriptTypeP2WPKH) // Estimate the largest outtx size in vByte - sizeMax := EstimateOuttxSize(21, []btcutil.Address{toP2TR}) + sizeMax, err := EstimateOuttxSize(21, []btcutil.Address{toP2TR}) + require.NoError(t, err) require.Equal(t, outTxBytesMax, sizeMax) // Estimate the smallest outtx size in vByte - sizeMin := EstimateOuttxSize(2, []btcutil.Address{toP2WPKH}) + sizeMin, err := EstimateOuttxSize(2, []btcutil.Address{toP2WPKH}) + require.NoError(t, err) require.Equal(t, outTxBytesMin, sizeMin) + + // Estimate unknown address type + nilP2PK := (*btcutil.AddressPubKey)(nil) + size, err := EstimateOuttxSize(1, []btcutil.Address{nilP2PK}) + require.Error(t, err) + require.Equal(t, uint64(0), size) } diff --git a/zetaclient/bitcoin/txscript.go b/zetaclient/bitcoin/tx_script.go similarity index 94% rename from zetaclient/bitcoin/txscript.go rename to zetaclient/bitcoin/tx_script.go index 73750f1803..c031a282a7 100644 --- a/zetaclient/bitcoin/txscript.go +++ b/zetaclient/bitcoin/tx_script.go @@ -11,20 +11,27 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" "github.com/cosmos/btcutil/base58" + "github.com/pkg/errors" "github.com/zeta-chain/zetacore/common" "github.com/zeta-chain/zetacore/common/bitcoin" "golang.org/x/crypto/ripemd160" - - "github.com/pkg/errors" ) const ( - // Length of P2TR, P2WSH, P2WPKH, P2SH, P2PKH scripts - LengthScriptP2TR = 34 - LengthScriptP2WSH = 34 + // Lenth of P2TR script [OP_1 0x20 <32-byte-hash>] + LengthScriptP2TR = 34 + + // Length of P2WSH script [OP_0 0x20 <32-byte-hash>] + LengthScriptP2WSH = 34 + + // Length of P2WPKH script [OP_0 0x14 <20-byte-hash>] LengthScriptP2WPKH = 22 - LengthScriptP2SH = 23 - LengthScriptP2PKH = 25 + + // Length of P2SH script [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] + LengthScriptP2SH = 23 + + // Length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] + LengthScriptP2PKH = 25 ) // PayToAddrScript creates a new script to pay a transaction output to a the @@ -40,25 +47,21 @@ func PayToAddrScript(addr btcutil.Address) ([]byte, error) { // IsPkScriptP2TR checks if the given script is a P2TR script func IsPkScriptP2TR(script []byte) bool { - // [OP_1 0x20 <32-byte-hash>] return len(script) == LengthScriptP2TR && script[0] == txscript.OP_1 && script[1] == 0x20 } // IsPkScriptP2WSH checks if the given script is a P2WSH script func IsPkScriptP2WSH(script []byte) bool { - // [OP_0 0x20 <32-byte-hash>] return len(script) == LengthScriptP2WSH && script[0] == txscript.OP_0 && script[1] == 0x20 } // IsPkScriptP2WPKH checks if the given script is a P2WPKH script func IsPkScriptP2WPKH(script []byte) bool { - // [OP_0 0x14 <20-byte-hash>] return len(script) == LengthScriptP2WPKH && script[0] == txscript.OP_0 && script[1] == 0x14 } // IsPkScriptP2SH checks if the given script is a P2SH script func IsPkScriptP2SH(script []byte) bool { - // [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] return len(script) == LengthScriptP2SH && script[0] == txscript.OP_HASH160 && script[1] == 0x14 && @@ -67,7 +70,6 @@ func IsPkScriptP2SH(script []byte) bool { // IsPkScriptP2PKH checks if the given script is a P2PKH script func IsPkScriptP2PKH(script []byte) bool { - // [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] return len(script) == LengthScriptP2PKH && script[0] == txscript.OP_DUP && script[1] == txscript.OP_HASH160 && diff --git a/zetaclient/bitcoin/txscript_test.go b/zetaclient/bitcoin/tx_script_test.go similarity index 100% rename from zetaclient/bitcoin/txscript_test.go rename to zetaclient/bitcoin/tx_script_test.go From edc9be607f5f551c3756277625beed1cf481c410 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 28 Mar 2024 15:39:09 -0500 Subject: [PATCH 15/16] removed panic from test method LoadObjectFromJSONFile --- zetaclient/bitcoin/bitcoin_client_test.go | 82 +++++++++---------- zetaclient/bitcoin/tx_script_test.go | 34 ++++---- zetaclient/compliance/compliance_test.go | 2 +- zetaclient/evm/evm_client_test.go | 2 +- zetaclient/evm/evm_signer_test.go | 26 +++--- zetaclient/evm/inbounds_test.go | 2 +- .../evm/outbound_transaction_data_test.go | 12 +-- zetaclient/testutils/testdata.go | 57 ++++++------- 8 files changed, 107 insertions(+), 110 deletions(-) diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index 1cf91c910f..21573a7cb5 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -42,13 +42,13 @@ func MockBTCClientMainnet() *BTCChainClient { } // createRPCClientAndLoadTx is a helper function to load raw tx and feed it to mock rpc client -func createRPCClientAndLoadTx(chainId int64, txHash string) *stub.MockBTCRPCClient { +func createRPCClientAndLoadTx(t *testing.T, chainId int64, txHash string) *stub.MockBTCRPCClient { // file name for the archived MsgTx nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainId, txHash)) // load archived MsgTx var msgTx wire.MsgTx - testutils.LoadObjectFromJSONFile(&msgTx, nameMsgTx) + testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) tx := btcutil.NewTx(&msgTx) // feed tx to mock rpc client @@ -96,11 +96,11 @@ func TestConfirmationThreshold(t *testing.T) { func TestAvgFeeRateBlock828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) + testutils.LoadObjectFromJSONFile(t, &blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) // https://mempool.space/block/000000000000000000025ca01d2c1094b8fd3bacc5468cc3193ced6a14618c27 var blockMb testutils.MempoolBlock - testutils.LoadObjectFromJSONFile(&blockMb, path.Join("../", testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json")) + testutils.LoadObjectFromJSONFile(t, &blockMb, path.Join("../", testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json")) gasRate, err := CalcBlockAvgFeeRate(&blockVb, &chaincfg.MainNetParams) require.NoError(t, err) @@ -110,7 +110,7 @@ func TestAvgFeeRateBlock828440(t *testing.T) { func TestAvgFeeRateBlock828440Errors(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) + testutils.LoadObjectFromJSONFile(t, &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{}} @@ -196,7 +196,7 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { func TestCalcDepositorFee828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) + testutils.LoadObjectFromJSONFile(t, &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) @@ -234,13 +234,13 @@ func TestCheckTSSVout(t *testing.T) { btcClient := MockBTCClientMainnet() t.Run("valid TSS vout should pass", func(t *testing.T) { - rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() 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(chainID, nonce) + _, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}, chain) @@ -250,7 +250,7 @@ func TestCheckTSSVout(t *testing.T) { 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) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() // invalid TSS vout @@ -259,7 +259,7 @@ func TestCheckTSSVout(t *testing.T) { 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus @@ -268,7 +268,7 @@ func TestCheckTSSVout(t *testing.T) { 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() // not match nonce mark @@ -277,7 +277,7 @@ func TestCheckTSSVout(t *testing.T) { 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus @@ -286,7 +286,7 @@ func TestCheckTSSVout(t *testing.T) { 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() // not match payment amount @@ -295,7 +295,7 @@ func TestCheckTSSVout(t *testing.T) { 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus @@ -317,7 +317,7 @@ 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() @@ -326,7 +326,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { - _, cctx := testutils.LoadBTCTxRawResultNCctx(chainID, nonce) + _, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}, chain) @@ -337,7 +337,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { }) 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() @@ -349,7 +349,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { }) 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() @@ -361,7 +361,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { }) 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(chainID, nonce) + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() @@ -381,7 +381,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // vin from the archived P2TR tx // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" - rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} @@ -393,7 +393,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // vin from the archived P2WSH tx // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 txHash := "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016" - rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} @@ -405,7 +405,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // vin from the archived P2WPKH tx // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 txHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" - rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} @@ -417,7 +417,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // vin from the archived P2SH tx // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a txHash := "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a" - rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} @@ -429,7 +429,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // vin from the archived P2PKH tx // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" - rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} @@ -443,7 +443,7 @@ func TestGetSenderAddressByVin(t *testing.T) { txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chain.ChainId, txHash)) var msgTx wire.MsgTx - testutils.LoadObjectFromJSONFile(&msgTx, nameMsgTx) + testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) // modify script to unknown script msgTx.TxOut[1].PkScript = []byte{0x00, 0x01, 0x02, 0x03} // can be any invalid script bytes @@ -485,7 +485,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { }) t.Run("should return error on invalid output index", func(t *testing.T) { // create mock rpc client with preloaded tx - rpcClient := createRPCClientAndLoadTx(chain.ChainId, txHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} sender, err := GetSenderAddressByVin(rpcClient, txVin, net) @@ -501,7 +501,7 @@ func TestGetBtcEvent(t *testing.T) { chain := common.BtcMainnetChain() // GetBtcEvent arguments - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tssAddress := testutils.TSSAddressBTCMainnet blockNumber := uint64(835640) net := &chaincfg.MainNetParams @@ -527,7 +527,7 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 2 eventExpected.FromAddress = "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) @@ -542,7 +542,7 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 2 eventExpected.FromAddress = "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) @@ -557,7 +557,7 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 0 eventExpected.FromAddress = "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) @@ -572,7 +572,7 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 0 eventExpected.FromAddress = "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) @@ -587,7 +587,7 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 1 eventExpected.FromAddress = "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(chain.ChainId, preHash) + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) @@ -596,7 +596,7 @@ func TestGetBtcEvent(t *testing.T) { }) t.Run("should skip tx if len(tx.Vout) < 2", func(t *testing.T) { // load tx and modify the tx to have only 1 vout - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vout = tx.Vout[:1] // get BTC event @@ -608,7 +608,7 @@ func TestGetBtcEvent(t *testing.T) { t.Run("should skip tx if Vout[0] is not a P2WPKH output", func(t *testing.T) { // load tx rpcClient := stub.NewMockBTCRPCClient() - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) @@ -624,7 +624,7 @@ func TestGetBtcEvent(t *testing.T) { }) t.Run("should skip tx if receiver address is not TSS address", func(t *testing.T) { // load tx and modify receiver address to any non-tss address: bc1qw8wrek2m7nlqldll66ajnwr9mh64syvkt67zlu - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" // get BTC event @@ -635,7 +635,7 @@ func TestGetBtcEvent(t *testing.T) { }) t.Run("should skip tx if amount is less than depositor fee", func(t *testing.T) { // load tx and modify amount to less than depositor fee - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee // get BTC event @@ -646,7 +646,7 @@ func TestGetBtcEvent(t *testing.T) { }) t.Run("should skip tx if 2nd vout is not OP_RETURN", func(t *testing.T) { // load tx and modify memo OP_RETURN to OP_1 - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a", "51", 1) // get BTC event @@ -657,7 +657,7 @@ func TestGetBtcEvent(t *testing.T) { }) t.Run("should skip tx if memo decoding fails", func(t *testing.T) { // load tx and modify memo length to be 1 byte less than actual - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a14", "6a13", 1) // get BTC event @@ -680,7 +680,7 @@ func TestGetBtcEventErrors(t *testing.T) { t.Run("should return error on invalid Vout[0] script", func(t *testing.T) { // load tx and modify Vout[0] script to invalid script - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vout[0].ScriptPubKey.Hex = "0014invalid000000000000000000000000000000000" // get BTC event @@ -691,7 +691,7 @@ func TestGetBtcEventErrors(t *testing.T) { }) t.Run("should return error if len(tx.Vin) < 1", func(t *testing.T) { // load tx and remove vin - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) tx.Vin = nil // get BTC event @@ -702,7 +702,7 @@ func TestGetBtcEventErrors(t *testing.T) { }) t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { // load tx and leave rpc client without preloaded tx - tx := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) rpcClient := stub.NewMockBTCRPCClient() // get BTC event diff --git a/zetaclient/bitcoin/tx_script_test.go b/zetaclient/bitcoin/tx_script_test.go index bf333ede59..9961acfcaf 100644 --- a/zetaclient/bitcoin/tx_script_test.go +++ b/zetaclient/bitcoin/tx_script_test.go @@ -22,7 +22,7 @@ func TestDecodeVoutP2TR(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) require.Len(t, rawResult.Vout, 2) // decode vout 0, P2TR @@ -40,7 +40,7 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] @@ -79,7 +79,7 @@ func TestDecodeVoutP2WSH(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) require.Len(t, rawResult.Vout, 1) // decode vout 0, P2WSH @@ -97,7 +97,7 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] @@ -136,7 +136,7 @@ func TestDecodeP2WPKHVout(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) require.Len(t, rawResult.Vout, 3) // decode vout 0, nonce mark 148 @@ -164,7 +164,7 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] @@ -196,7 +196,7 @@ func TestDecodeVoutP2SH(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) require.Len(t, rawResult.Vout, 2) // decode vout 0, P2SH @@ -214,7 +214,7 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] @@ -259,7 +259,7 @@ func TestDecodeVoutP2PKH(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) require.Len(t, rawResult.Vout, 2) // decode vout 0, P2PKH @@ -277,7 +277,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] @@ -334,7 +334,7 @@ func TestDecodeOpReturnMemo(t *testing.T) { chain := common.BtcMainnetChain() txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" - rawResult := testutils.LoadBTCIntxRawResult(chain.ChainId, txHash, false) + rawResult := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) require.True(t, len(rawResult.Vout) >= 2) require.Equal(t, scriptHex, rawResult.Vout[1].ScriptPubKey.Hex) @@ -410,7 +410,7 @@ func TestDecodeTSSVout(t *testing.T) { txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) @@ -423,7 +423,7 @@ func TestDecodeTSSVout(t *testing.T) { txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) receiverExpected := "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc" receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) @@ -436,7 +436,7 @@ func TestDecodeTSSVout(t *testing.T) { txHash := "5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b" nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WPKH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) receiverExpected := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) @@ -449,7 +449,7 @@ func TestDecodeTSSVout(t *testing.T) { txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) receiverExpected := "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE" receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) @@ -462,7 +462,7 @@ func TestDecodeTSSVout(t *testing.T) { txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) receiverExpected := "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte" receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) @@ -480,7 +480,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) var rawResult btcjson.TxRawResult - testutils.LoadObjectFromJSONFile(&rawResult, nameTx) + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" t.Run("should return error on invalid amount", func(t *testing.T) { diff --git a/zetaclient/compliance/compliance_test.go b/zetaclient/compliance/compliance_test.go index 93ce21f785..001e5d9e0d 100644 --- a/zetaclient/compliance/compliance_test.go +++ b/zetaclient/compliance/compliance_test.go @@ -14,7 +14,7 @@ import ( func TestCctxRestricted(t *testing.T) { // load archived cctx var cctx crosschaintypes.CrossChainTx - testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_1_6270.json")) + testutils.LoadObjectFromJSONFile(t, &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 d00b4ffcfb..6f4cb677ed 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(chainID, blockNumber, true) + block := testutils.LoadEVMBlock(t, 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 c318a54de5..90676cc3d8 100644 --- a/zetaclient/evm/evm_signer_test.go +++ b/zetaclient/evm/evm_signer_test.go @@ -65,15 +65,15 @@ func getNewOutTxProcessor() *outtxprocessor.Processor { return outtxprocessor.NewOutTxProcessorManager(logger) } -func getCCTX() *crosschaintypes.CrossChainTx { +func getCCTX(t *testing.T) *crosschaintypes.CrossChainTx { var cctx crosschaintypes.CrossChainTx - testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) + testutils.LoadObjectFromJSONFile(t, &cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) return &cctx } -func getInvalidCCTX() *crosschaintypes.CrossChainTx { +func getInvalidCCTX(t *testing.T) *crosschaintypes.CrossChainTx { var cctx crosschaintypes.CrossChainTx - testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) + testutils.LoadObjectFromJSONFile(t, &cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) return &cctx } @@ -104,7 +104,7 @@ func TestSigner_SetGetERC20CustodyAddress(t *testing.T) { func TestSigner_TryProcessOutTx(t *testing.T) { evmSigner, err := getNewEvmSigner() require.NoError(t, err) - cctx := getCCTX() + cctx := getCCTX(t) processorManager := getNewOutTxProcessor() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) @@ -123,7 +123,7 @@ func TestSigner_SignOutboundTx(t *testing.T) { // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -152,7 +152,7 @@ func TestSigner_SignRevertTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -181,7 +181,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -210,7 +210,7 @@ func TestSigner_SignCommandTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -257,7 +257,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -286,7 +286,7 @@ func TestSigner_BroadcastOutTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -316,7 +316,7 @@ func TestSigner_getEVMRPC(t *testing.T) { } func TestSigner_SignerErrorMsg(t *testing.T) { - cctx := getCCTX() + cctx := getCCTX(t) msg := SignerErrorMsg(cctx) require.Contains(t, msg, "nonce 68270 chain 56") @@ -328,7 +328,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx := getCCTX() + cctx := getCCTX(t) 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 fdac9770a0..e382340590 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(chainID, blockNumber, true) + block := testutils.LoadEVMBlock(t, chainID, blockNumber, true) // create mock client evmClient := stub.NewMockEvmClient() diff --git a/zetaclient/evm/outbound_transaction_data_test.go b/zetaclient/evm/outbound_transaction_data_test.go index abf9656665..8f081d9440 100644 --- a/zetaclient/evm/outbound_transaction_data_test.go +++ b/zetaclient/evm/outbound_transaction_data_test.go @@ -13,7 +13,7 @@ import ( func TestSigner_SetChainAndSender(t *testing.T) { // setup inputs - cctx := getCCTX() + cctx := getCCTX(t) txData := &OutBoundTransactionData{} logger := zerolog.Logger{} @@ -43,7 +43,7 @@ func TestSigner_SetChainAndSender(t *testing.T) { } func TestSigner_SetupGas(t *testing.T) { - cctx := getCCTX() + cctx := getCCTX(t) evmSigner, err := getNewEvmSigner() require.NoError(t, err) @@ -73,14 +73,14 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { require.NoError(t, err) t.Run("NewOutBoundTransactionData success", func(t *testing.T) { - cctx := getCCTX() + cctx := getCCTX(t) _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) }) t.Run("NewOutBoundTransactionData skip", func(t *testing.T) { - cctx := getCCTX() + cctx := getCCTX(t) cctx.CctxStatus.Status = types.CctxStatus_Aborted _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.NoError(t, err) @@ -88,7 +88,7 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { }) t.Run("NewOutBoundTransactionData unknown chain", func(t *testing.T) { - cctx := getInvalidCCTX() + cctx := getInvalidCCTX(t) require.NoError(t, err) _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.ErrorContains(t, err, "unknown chain") @@ -96,7 +96,7 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { }) t.Run("NewOutBoundTransactionData setup gas error", func(t *testing.T) { - cctx := getCCTX() + cctx := getCCTX(t) require.NoError(t, err) cctx.GetCurrentOutTxParam().OutboundTxGasPrice = "invalidGasPrice" _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index 79311fdf52..caf9324958 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -10,6 +10,7 @@ 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" @@ -37,19 +38,15 @@ func SaveObjectToJSONFile(obj interface{}, filename string) error { } // LoadObjectFromJSONFile loads an object from a file in JSON format -func LoadObjectFromJSONFile(obj interface{}, filename string) { +func LoadObjectFromJSONFile(t *testing.T, obj interface{}, filename string) { file, err := os.Open(filepath.Clean(filename)) - if err != nil { - panic(err) - } + require.NoError(t, err) defer file.Close() // read the struct from the file decoder := json.NewDecoder(file) err = decoder.Decode(&obj) - if err != nil { - panic(err) - } + require.NoError(t, err) } func ComplianceConfigTest() config.ComplianceConfig { @@ -80,82 +77,82 @@ func SaveBTCBlockTrimTx(blockVb *btcjson.GetBlockVerboseTxResult, filename strin } // LoadEVMBlock loads archived evm block from file -func LoadEVMBlock(chainID int64, blockNumber uint64, trimmed bool) *ethrpc.Block { +func LoadEVMBlock(t *testing.T, chainID int64, blockNumber uint64, trimmed bool) *ethrpc.Block { name := path.Join("../", TestDataPathEVM, FileNameEVMBlock(chainID, blockNumber, trimmed)) block := ðrpc.Block{} - LoadObjectFromJSONFile(block, name) + LoadObjectFromJSONFile(t, block, name) return block } // LoadBTCIntxRawResult loads archived Bitcoin intx raw result from file -func LoadBTCIntxRawResult(chainID int64, txHash string, donation bool) *btcjson.TxRawResult { +func LoadBTCIntxRawResult(t *testing.T, chainID int64, txHash string, donation bool) *btcjson.TxRawResult { name := path.Join("../", TestDataPathBTC, FileNameBTCIntx(chainID, txHash, donation)) rawResult := &btcjson.TxRawResult{} - LoadObjectFromJSONFile(rawResult, name) + LoadObjectFromJSONFile(t, rawResult, name) return rawResult } // LoadBTCTxRawResultNCctx loads archived Bitcoin outtx raw result and corresponding cctx -func LoadBTCTxRawResultNCctx(chainID int64, nonce uint64) (*btcjson.TxRawResult, *crosschaintypes.CrossChainTx) { +func LoadBTCTxRawResultNCctx(t *testing.T, chainID int64, nonce uint64) (*btcjson.TxRawResult, *crosschaintypes.CrossChainTx) { //nameTx := FileNameBTCOuttx(chainID, nonce) nameTx := path.Join("../", TestDataPathBTC, FileNameBTCOuttx(chainID, nonce)) rawResult := &btcjson.TxRawResult{} - LoadObjectFromJSONFile(rawResult, nameTx) + LoadObjectFromJSONFile(t, rawResult, nameTx) nameCctx := path.Join("../", TestDataPathCctx, FileNameCctxByNonce(chainID, nonce)) cctx := &crosschaintypes.CrossChainTx{} - LoadObjectFromJSONFile(cctx, nameCctx) + LoadObjectFromJSONFile(t, cctx, nameCctx) return rawResult, cctx } // LoadEVMIntx loads archived intx from file func LoadEVMIntx( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethrpc.Transaction { nameTx := path.Join("../", TestDataPathEVM, FileNameEVMIntx(chainID, intxHash, coinType, false)) tx := ðrpc.Transaction{} - LoadObjectFromJSONFile(&tx, nameTx) + LoadObjectFromJSONFile(t, &tx, nameTx) return tx } // LoadEVMIntxReceipt loads archived intx receipt from file func LoadEVMIntxReceipt( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Receipt { nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMIntxReceipt(chainID, intxHash, coinType, false)) receipt := ðtypes.Receipt{} - LoadObjectFromJSONFile(&receipt, nameReceipt) + LoadObjectFromJSONFile(t, &receipt, nameReceipt) return receipt } // LoadEVMIntxCctx loads archived intx cctx from file func LoadEVMIntxCctx( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *crosschaintypes.CrossChainTx { nameCctx := path.Join("../", TestDataPathCctx, FileNameEVMIntxCctx(chainID, intxHash, coinType)) cctx := &crosschaintypes.CrossChainTx{} - LoadObjectFromJSONFile(&cctx, nameCctx) + LoadObjectFromJSONFile(t, &cctx, nameCctx) return cctx } // LoadCctxByNonce loads archived cctx by nonce from file func LoadCctxByNonce( - _ *testing.T, + t *testing.T, chainID int64, nonce uint64) *crosschaintypes.CrossChainTx { nameCctx := path.Join("../", TestDataPathCctx, FileNameCctxByNonce(chainID, nonce)) cctx := &crosschaintypes.CrossChainTx{} - LoadObjectFromJSONFile(&cctx, nameCctx) + LoadObjectFromJSONFile(t, &cctx, nameCctx) return cctx } @@ -174,27 +171,27 @@ func LoadEVMIntxNReceipt( // LoadEVMIntxDonation loads archived donation intx from file func LoadEVMIntxDonation( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethrpc.Transaction { nameTx := path.Join("../", TestDataPathEVM, FileNameEVMIntx(chainID, intxHash, coinType, true)) tx := ðrpc.Transaction{} - LoadObjectFromJSONFile(&tx, nameTx) + LoadObjectFromJSONFile(t, &tx, nameTx) return tx } // LoadEVMIntxReceiptDonation loads archived donation intx receipt from file func LoadEVMIntxReceiptDonation( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Receipt { nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMIntxReceipt(chainID, intxHash, coinType, true)) receipt := ðtypes.Receipt{} - LoadObjectFromJSONFile(&receipt, nameReceipt) + LoadObjectFromJSONFile(t, &receipt, nameReceipt) return receipt } @@ -227,27 +224,27 @@ func LoadEVMIntxNReceiptNCctx( // LoadEVMOuttx loads archived evm outtx from file func LoadEVMOuttx( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Transaction { nameTx := path.Join("../", TestDataPathEVM, FileNameEVMOuttx(chainID, intxHash, coinType)) tx := ðtypes.Transaction{} - LoadObjectFromJSONFile(&tx, nameTx) + LoadObjectFromJSONFile(t, &tx, nameTx) return tx } // LoadEVMOuttxReceipt loads archived evm outtx receipt from file func LoadEVMOuttxReceipt( - _ *testing.T, + t *testing.T, chainID int64, intxHash string, coinType common.CoinType) *ethtypes.Receipt { nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMOuttxReceipt(chainID, intxHash, coinType)) receipt := ðtypes.Receipt{} - LoadObjectFromJSONFile(&receipt, nameReceipt) + LoadObjectFromJSONFile(t, &receipt, nameReceipt) return receipt } From f02cc5253a5fbc6f0402c41ad0905837de3eb2c9 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 28 Mar 2024 16:41:43 -0500 Subject: [PATCH 16/16] improved function and added comments --- common/bitcoin/address_taproot.go | 1 + zetaclient/bitcoin/bitcoin_client.go | 36 ++++++++++------------- zetaclient/bitcoin/bitcoin_client_test.go | 31 +++++++++---------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/common/bitcoin/address_taproot.go b/common/bitcoin/address_taproot.go index e178a8e5a3..fa9968dcfe 100644 --- a/common/bitcoin/address_taproot.go +++ b/common/bitcoin/address_taproot.go @@ -193,6 +193,7 @@ func (a *AddressSegWit) String() string { return a.EncodeAddress() } +// DecodeTaprootAddress decodes taproot address only and returns error on non-taproot address func DecodeTaprootAddress(addr string) (*AddressTaproot, error) { hrp, version, program, err := decodeSegWitAddress(addr) if err != nil { diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 2a3afbb9f7..327db5a941 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -726,7 +726,7 @@ func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { return false } -// GetBtcEvent either returns a valid BTCInTxEvnet or nil +// GetBtcEvent either returns a valid BTCInTxEvent or nil // Note: the caller should retry the tx on error (e.g., GetSenderAddressByVin failed) func GetBtcEvent( rpcClient interfaces.BTCRPCClient, @@ -741,7 +741,7 @@ func GetBtcEvent( var value float64 var memo []byte if len(tx.Vout) >= 2 { - // 1st vout must to addressed to the tssAddress with p2wpkh scriptPubKey + // 1st vout must have tss address as receiver with p2wpkh scriptPubKey vout0 := tx.Vout[0] script := vout0.ScriptPubKey.Hex if len(script) == 44 && script[:4] == "0014" { // P2WPKH output: 0x00 + 20 bytes of pubkey hash @@ -1252,12 +1252,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, ob.chain) + err = ob.checkTSSVoutCancelled(params, rawResult.Vout) 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, ob.chain) + err = ob.checkTSSVout(params, rawResult.Vout) if err != nil { return errors.Wrapf(err, "checkTssOutTxResult: invalid TSS Vout in outTx %s nonce %d", hash, nonce) } @@ -1343,7 +1343,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, chain common.Chain) error { +func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []btcjson.Vout) 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)) @@ -1358,21 +1358,19 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b // the 2nd output is the payment to recipient receiverExpected = params.Receiver } - receiverVout, amount, err := DecodeTSSVout(vout, receiverExpected, chain) + receiverVout, amount, err := DecodeTSSVout(vout, receiverExpected, ob.chain) if err != nil { return err } - // 1st vout: nonce-mark - if vout.N == 0 { + switch vout.N { + case 0: // 1st vout: nonce-mark 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)) } - } - // 2nd vout: payment to recipient - if vout.N == 1 { + case 1: // 2nd vout: payment to recipient if receiverVout != params.Receiver { return fmt.Errorf("checkTSSVout: output address %s not match params receiver %s", receiverVout, params.Receiver) } @@ -1380,9 +1378,7 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b if uint64(amount) != params.Amount.Uint64() { return fmt.Errorf("checkTSSVout: output amount %d not match params amount %d", amount, params.Amount) } - } - // 3rd vout: change to TSS (optional) - if vout.N == 2 { + case 2: // 3rd vout: change to TSS (optional) if receiverVout != tssAddress { return fmt.Errorf("checkTSSVout: change address %s not match TSS address %s", receiverVout, tssAddress) } @@ -1394,7 +1390,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, chain common.Chain) error { +func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, vouts []btcjson.Vout) 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)) @@ -1404,21 +1400,19 @@ func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := DecodeTSSVout(vout, tssAddress, chain) + receiverVout, amount, err := DecodeTSSVout(vout, tssAddress, ob.chain) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } - // 1st vout: nonce-mark - if vout.N == 0 { + switch vout.N { + case 0: // 1st vout: nonce-mark 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)) } - } - // 2nd vout: change to TSS (optional) - if vout.N == 2 { + case 1: // 2nd vout: change to TSS (optional) if receiverVout != tssAddress { return fmt.Errorf("checkTSSVoutCancelled: change address %s not match TSS address %s", receiverVout, tssAddress) } diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index 21573a7cb5..8eb7a62c14 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -236,17 +236,17 @@ func TestCheckTSSVout(t *testing.T) { t.Run("valid TSS vout should pass", func(t *testing.T) { rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 2 or > 3", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}, chain) + err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}, chain) + err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail on invalid TSS vout", func(t *testing.T) { @@ -255,7 +255,7 @@ func TestCheckTSSVout(t *testing.T) { // invalid TSS vout rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.Error(t, err) }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -264,7 +264,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -273,7 +273,7 @@ func TestCheckTSSVout(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) 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) { @@ -282,7 +282,7 @@ func TestCheckTSSVout(t *testing.T) { // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params receiver") }) t.Run("should fail if vout 1 not match payment amount", func(t *testing.T) { @@ -291,7 +291,7 @@ func TestCheckTSSVout(t *testing.T) { // not match payment amount rawResult.Vout[1].Value = 0.00011000 - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) 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) { @@ -300,7 +300,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[2].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout, chain) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } @@ -322,17 +322,17 @@ func TestCheckTSSVoutCancelled(t *testing.T) { rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() - err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}, chain) + err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}, chain) + err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) 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) { @@ -344,7 +344,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -356,19 +356,20 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) 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.Vout[1] = rawResult.Vout[2] + rawResult.Vout[1].N = 1 // swap vout index rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout, chain) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) }