Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Bitcoin P2PKH (legacy), P2SH, P2TR address types #1842

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 34 additions & 2 deletions common/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
255 changes: 254 additions & 1 deletion common/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading