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 diff --git a/common/address.go b/common/address.go index 6a2cdd7df1..763b23fd94 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" ) type Address string @@ -51,6 +52,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 { @@ -63,13 +65,43 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre if err != nil { return nil, err } + if chainParams == nil { + return nil, fmt.Errorf("chain params not found") + } + // test taproot address type + address, err = bitcoin.DecodeTaprootAddress(inputAddress) + 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 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 dd78dda391..1ec0e405ec 100644 --- a/common/address_test.go +++ b/common/address_test.go @@ -4,7 +4,9 @@ import ( "errors" "testing" + "github.com/btcsuite/btcutil" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/common/bitcoin" . "gopkg.in/check.v1" ) @@ -65,12 +67,263 @@ 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) 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.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) + }) + } } func TestConvertRecoverToError(t *testing.T) { diff --git a/common/bitcoin/address_taproot.go b/common/bitcoin/address_taproot.go new file mode 100644 index 0000000000..e178a8e5a3 --- /dev/null +++ b/common/bitcoin/address_taproot.go @@ -0,0 +1,217 @@ +package bitcoin + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/btcsuite/btcd/btcutil/bech32" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "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 nil, err + } + if version != 1 { + return nil, errors.New("invalid witness version; taproot address must be version 1") + } + return &AddressTaproot{ + AddressSegWit{ + hrp: hrp, + witnessVersion: version, + witnessProgram: program, + }, + }, 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 new file mode 100644 index 0000000000..0082b450eb --- /dev/null +++ b/common/bitcoin/address_taproot_test.go @@ -0,0 +1,75 @@ +package bitcoin + +import ( + "encoding/hex" + "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) + 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) + 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) + 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) + } + { + // 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") + require.Nil(t, err) + addr, err := NewAddressTaproot(witnessProg[:], &chaincfg.MainNetParams) + 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())) + } +} 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..80234c1265 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -265,14 +265,18 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( } depositorFee := zetabitcoin.DefaultDepositorFee - events := zetabitcoin.FilterAndParseIncomingTx( + events, err := zetabitcoin.FilterAndParseIncomingTx( + btcRPC, []btcjson.TxRawResult{*rawtx}, 0, runner.BTCTSSAddress.EncodeAddress(), - &log.Logger, + log.Logger, 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/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..72b44d5804 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") }) } @@ -714,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/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 514447f0d9..2a3afbb9f7 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,14 +447,19 @@ 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), tssAddress, - &ob.logger.WatchInTx, + ob.logger.WatchInTx, 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 { @@ -651,29 +655,30 @@ 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 { +) ([]*BTCInTxEvnet, error) { inTxs := make([]*BTCInTxEvnet, 0) for idx, tx := range txs { 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 + // 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,11 +726,14 @@ func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { return false } +// 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, - targetAddress string, + tssAddress string, blockNumber uint64, - logger *zerolog.Logger, + logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, ) (*BTCInTxEvnet, error) { @@ -733,74 +741,46 @@ 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 { + // 1st vout must to addressed to the tssAddress 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 + receiver, err := DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) + if err != nil { // should never happen return nil, err } - if wpkhAddress.EncodeAddress() != targetAddress { - return nil, nil // irrelevant tx to us, skip + // skip irrelevant tx to us + if receiver != tssAddress { + return nil, nil } // 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) + 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 = out.Value - depositorFee + value = vout0.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 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] - //log.Info().Msgf("vin: %v", vin.Witness) - if len(vin.Witness) == 2 { - 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() - } + 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, - ToAddress: targetAddress, + ToAddress: tssAddress, Value: value, MemoBytes: memo, BlockNumber: blockNumber, @@ -810,6 +790,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 { @@ -953,7 +972,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) } @@ -973,7 +992,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 } @@ -1148,7 +1167,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 @@ -1222,7 +1241,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()) } @@ -1233,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) + 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) } @@ -1246,23 +1265,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) } @@ -1272,7 +1293,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) } @@ -1322,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) 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)) @@ -1331,14 +1352,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)) @@ -1346,8 +1373,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() { @@ -1356,8 +1383,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) } } } @@ -1367,7 +1394,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)) @@ -1376,14 +1403,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)) @@ -1391,8 +1419,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 0c8870a525..1fe18d14a5 100644 --- a/zetaclient/bitcoin/bitcoin_client_rpc_test.go +++ b/zetaclient/bitcoin/bitcoin_client_rpc_test.go @@ -145,15 +145,16 @@ 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), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - &log.Logger, + log.Logger, &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") @@ -182,15 +183,16 @@ 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), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - &log.Logger, + log.Logger, &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 81edf59bf5..b1fc32b8c7 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" @@ -46,7 +48,7 @@ func TestNewBitcoinClient(t *testing.T) { appContext := appcontext.NewAppContext(coreContext, cfg) chain := common.BtcMainnetChain() bridge := stub.NewMockZetaCoreBridge() - tss := stub.NewMockTSS(sample.EthAddress().String(), "") + tss := stub.NewMockTSS(common.BtcTestNetChain(), sample.EthAddress().String(), "") loggers := clientcommon.ClientLogger{} btcCfg := cfg.BitcoinConfig ts := metrics.NewTelemetryServer() @@ -78,13 +80,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) @@ -94,8 +94,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{}} @@ -181,8 +180,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) @@ -212,71 +210,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") }) } @@ -284,7 +292,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 @@ -292,62 +301,417 @@ 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") }) } +// 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) + }) +} + func TestBTCChainClient_ObserveInTx(t *testing.T) { t.Run("should return error", func(t *testing.T) { // create mainnet mock client diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index 01e05715ab..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 @@ -97,9 +98,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, @@ -136,14 +198,9 @@ 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) + 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) } @@ -162,45 +219,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) @@ -312,27 +335,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 @@ -354,7 +363,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 5c12e54b11..575b8f0d09 100644 --- a/zetaclient/bitcoin/bitcoin_signer_test.go +++ b/zetaclient/bitcoin/bitcoin_signer_test.go @@ -4,12 +4,12 @@ import ( "encoding/hex" "fmt" "math" - "math/rand" + "math/big" + "reflect" "sort" "sync" "testing" - "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" @@ -25,6 +25,7 @@ import ( corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" . "gopkg.in/check.v1" ) @@ -34,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 @@ -104,7 +80,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) @@ -176,7 +152,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) @@ -197,7 +173,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) { @@ -239,193 +215,7 @@ 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 := PayToWitnessPubKeyHashScript(addr.WitnessProgram()) - 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 TestP2WPHSize2In3Out(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 TestP2WPHSize21In3Out(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 TestP2WPHSizeXIn3Out(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 TestP2WPHSizeBreakdown(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 new BitcoinChainClient +// helper function to create a test BitcoinChainClient func createTestClient(t *testing.T) *BTCChainClient { skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" privateKey, err := crypto.HexToECDSA(skHex) @@ -433,14 +223,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) @@ -451,6 +245,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{}, nil) + 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) @@ -471,7 +398,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 @@ -563,7 +490,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 @@ -577,7 +504,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 @@ -591,7 +518,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 @@ -610,7 +537,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 @@ -629,7 +556,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 @@ -647,7 +574,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 @@ -685,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 de73756be4..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 { @@ -109,7 +108,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/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..5ed7a6c54e --- /dev/null +++ b/zetaclient/bitcoin/fee_test.go @@ -0,0 +1,416 @@ +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 + vError := uint64(1) // 1 vByte error tolerance + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateOuttxSize(uint64(len(utxosTxids)), []btcutil.Address{payee}) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + } +} + +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 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 { + 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/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..73750f1803 --- /dev/null +++ b/zetaclient/bitcoin/txscript.go @@ -0,0 +1,227 @@ +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 ( + // 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 +// 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) + } +} + +// 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 script %s", scriptHex) + } + if !IsPkScriptP2TR(script) { + return "", fmt.Errorf("invalid P2TR script: %s", scriptHex) + } + witnessProg := script[2:] + receiverAddress, err := bitcoin.NewAddressTaproot(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting address from script %s", scriptHex) + } + return receiverAddress.EncodeAddress(), nil +} + +// 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 script: %s", scriptHex) + } + if !IsPkScriptP2WSH(script) { + return "", fmt.Errorf("invalid P2WSH script: %s", scriptHex) + } + witnessProg := script[2:] + receiverAddress, err := btcutil.NewAddressWitnessScriptHash(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) + } + return receiverAddress.EncodeAddress(), nil +} + +// 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 script: %s", scriptHex) + } + if !IsPkScriptP2WPKH(script) { + return "", fmt.Errorf("invalid P2WPKH script: %s", scriptHex) + } + witnessProg := script[2:] + receiverAddress, err := btcutil.NewAddressWitnessPubKeyHash(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) + } + return receiverAddress.EncodeAddress(), nil +} + +// 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 script: %s", scriptHex) + } + if !IsPkScriptP2SH(script) { + return "", fmt.Errorf("invalid P2SH script: %s", scriptHex) + } + scriptHash := script[2:22] + return EncodeAddress(scriptHash, net.ScriptHashAddrID), nil +} + +// 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 script: %s", scriptHex) + } + if !IsPkScriptP2PKH(script) { + return "", fmt.Errorf("invalid P2PKH script: %s", scriptHex) + } + pubKeyHash := script[3:23] + return EncodeAddress(pubKeyHash, net.PubKeyHashAddrID), nil +} + +// DecodeOpReturnMemo decodes memo from OP_RETURN script +// returns (memo, found, error) +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", scriptHex) + } + 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(scriptHex[4:]) + if err != nil { + 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) + } + 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 = DecodeScriptP2TR(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressWitnessScriptHash: + receiverVout, err = DecodeScriptP2WSH(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressWitnessPubKeyHash: + receiverVout, err = DecodeScriptP2WPKH(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressScriptHash: + receiverVout, err = DecodeScriptP2SH(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressPubKeyHash: + receiverVout, err = DecodeScriptP2PKH(vout.ScriptPubKey.Hex, 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..bf333ede59 --- /dev/null +++ b/zetaclient/bitcoin/txscript_test.go @@ -0,0 +1,520 @@ +package bitcoin + +import ( + "encoding/hex" + "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 := DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, 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 invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, 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 := 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 := 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 := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") + }) +} + +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 := DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, 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 invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, 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 := 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 := 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 := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") + }) +} + +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 := 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 = 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 = DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, 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 invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, 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 := 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 := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WPKH script") + }) +} + +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 := DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, 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 invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, 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 := 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 := 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 := 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 := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") + }) +} + +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 := DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, 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 invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, 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 := 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 := 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 := 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 := 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 := 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 := 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) + }) +} + +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..fe2bb2481b 100644 --- a/zetaclient/bitcoin/utils.go +++ b/zetaclient/bitcoin/utils.go @@ -1,50 +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/txscript" - "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, @@ -57,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. @@ -221,34 +47,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 656908ce51..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,16 +65,16 @@ func getNewOutTxProcessor() *outtxprocessor.Processor { return outtxprocessor.NewOutTxProcessorManager(logger) } -func getCCTX() (*types.CrossChainTx, error) { +func getCCTX() *crosschaintypes.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() *crosschaintypes.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) { @@ -105,8 +104,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) @@ -125,8 +123,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) @@ -155,8 +152,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) @@ -185,8 +181,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) @@ -215,8 +210,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) @@ -263,8 +257,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) @@ -293,8 +286,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) @@ -324,8 +316,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") @@ -337,8 +328,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/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) 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/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_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/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/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/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..79311fdf52 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,82 @@ 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 } +// 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(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 +174,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 +227,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..eb4516f4d0 100644 --- a/zetaclient/testutils/testdata_naming.go +++ b/zetaclient/testutils/testdata_naming.go @@ -48,6 +48,17 @@ 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) +} + +// 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)