Skip to content

Commit

Permalink
initial commit to support different types of BTC addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
ws4charlie committed Mar 22, 2024
1 parent a60375c commit c2dfb7a
Show file tree
Hide file tree
Showing 34 changed files with 1,908 additions and 409 deletions.
32 changes: 27 additions & 5 deletions common/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func ConvertRecoverToError(r interface{}) error {
}
}

// DecodeBtcAddress decodes a BTC address from a given string and chainID
func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Address, err error) {
defer func() {
if r := recover(); r != nil {
Expand All @@ -74,17 +75,38 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre
}
// test taproot address type
address, err = bitcoin.DecodeTaprootAddress(inputAddress)
if err == nil && address.IsForNet(chainParams) {
return address, nil
if err == nil {
if address.IsForNet(chainParams) {
return address, nil
}
return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name)
}
// test taproot address fail; continue other types
// test taproot address failed; continue testing other types: P2WSH, P2WPKH, P2SH, P2PKH
address, err = btcutil.DecodeAddress(inputAddress, chainParams)
if err != nil {
return nil, fmt.Errorf("decode address failed: %s , for input address %s", err.Error(), inputAddress)
return nil, fmt.Errorf("decode address failed: %s, for input address %s", err.Error(), inputAddress)
}
ok := address.IsForNet(chainParams)
if !ok {
return nil, fmt.Errorf("address is not for network %s", chainParams.Name)
return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name)
}
return
}

// IsBtcAddressSupported returns true if the given BTC address is supported
func IsBtcAddressSupported(addr btcutil.Address) bool {
switch addr.(type) {
// P2TR address
case *bitcoin.AddressTaproot,
// P2WSH address
*btcutil.AddressWitnessScriptHash,
// P2WPKH address
*btcutil.AddressWitnessPubKeyHash,
// P2SH address
*btcutil.AddressScriptHash,
// P2PKH address
*btcutil.AddressPubKeyHash:
return true
}
return false
}
248 changes: 246 additions & 2 deletions common/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package common
import (
"testing"

"github.com/btcsuite/btcutil"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/zetacore/common/bitcoin"

. "gopkg.in/check.v1"
)
Expand Down Expand Up @@ -54,7 +56,7 @@ func TestDecodeBtcAddress(t *testing.T) {

t.Run("non legacy valid address with incorrect params", func(t *testing.T) {
_, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcMainnetChain().ChainId)
require.ErrorContains(t, err, "address is not for network mainnet")
require.ErrorContains(t, err, "not for network mainnet")
})
t.Run("non legacy valid address with correct params", func(t *testing.T) {
_, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcRegtestChain().ChainId)
Expand All @@ -67,6 +69,248 @@ func TestDecodeBtcAddress(t *testing.T) {
})
t.Run("taproot address with incorrect params", func(t *testing.T) {
_, err := DecodeBtcAddress("bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c", BtcTestNetChain().ChainId)
require.Error(t, err)
require.ErrorContains(t, err, "not for network testnet")
})
}

func Test_IsBtcAddressSupported_P2TR(t *testing.T) {
tests := []struct {
name string
addr string
chainId int64
supported bool
}{
{
// https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7
name: "mainnet taproot address",
addr: "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9",
chainId: BtcMainnetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/24991bd2fdc4f744bf7bbd915d4915925eecebdae249f81e057c0a6ffb700ab9
name: "testnet taproot address",
addr: "tb1p7qqaucx69xtwkx7vwmhz03xjmzxxpy3hk29y7q06mt3k6a8sehhsu5lacw",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
name: "regtest taproot address",
addr: "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// it should be a taproot address
addr, err := DecodeBtcAddress(tt.addr, tt.chainId)
require.NoError(t, err)
_, ok := addr.(*bitcoin.AddressTaproot)
require.True(t, ok)

// it should be supported
require.NoError(t, err)
supported := IsBtcAddressSupported(addr)
require.Equal(t, tt.supported, supported)
})
}
}

func Test_IsBtcAddressSupported_P2WSH(t *testing.T) {
tests := []struct {
name string
addr string
chainId int64
supported bool
}{
{
// https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53
name: "mainnet P2WSH address",
addr: "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc",
chainId: BtcMainnetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/78fac3f0d4c0174c88d21c4bb1e23a8f007e890c6d2cfa64c97389ead16c51ed
name: "testnet P2WSH address",
addr: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
name: "regtest P2WSH address",
addr: "bcrt1qm9mzhyky4w853ft2ms6dtqdyyu3z2tmrq8jg8xglhyuv0dsxzmgs2f0sqy",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// it should be a P2WSH address
addr, err := DecodeBtcAddress(tt.addr, tt.chainId)
require.NoError(t, err)
_, ok := addr.(*btcutil.AddressWitnessScriptHash)
require.True(t, ok)

// it should be supported
supported := IsBtcAddressSupported(addr)
require.Equal(t, tt.supported, supported)
})
}
}

func Test_IsBtcAddressSupported_P2WPKH(t *testing.T) {
tests := []struct {
name string
addr string
chainId int64
supported bool
}{
{
// https://mempool.space/tx/5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b
name: "mainnet P2WPKH address",
addr: "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y",
chainId: BtcMainnetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/508b4d723c754bad001eae9b7f3c12377d3307bd5b595c27fd8a90089094f0e9
name: "testnet P2WPKH address",
addr: "tb1q6rufg6myrxurdn0h57d2qhtm9zfmjw2mzcm05q",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
name: "regtest P2WPKH address",
addr: "bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// it should be a P2WPKH address
addr, err := DecodeBtcAddress(tt.addr, tt.chainId)
require.NoError(t, err)
_, ok := addr.(*btcutil.AddressWitnessPubKeyHash)
require.True(t, ok)

// it should be supported
supported := IsBtcAddressSupported(addr)
require.Equal(t, tt.supported, supported)
})
}
}

func Test_IsBtcAddressSupported_P2SH(t *testing.T) {
tests := []struct {
name string
addr string
chainId int64
supported bool
}{
{
// https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21
name: "mainnet P2SH address",
addr: "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE",
chainId: BtcMainnetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/0c8c8f94817e0288a5273f5c971adaa3cee18a895c3ec8544785dddcd96f3848
name: "testnet P2SH address 1",
addr: "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/b5e074c5e021fcbd91ea14b1db29dfe5d14e1a6e046039467bf6ada7f8cc01b3
name: "testnet P2SH address 2",
addr: "2MwbFpRpZWv4zREjbdLB9jVW3Q8xonpVeyE",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
name: "testnet P2SH address 1 should also be supported in regtest",
addr: "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
{
name: "testnet P2SH address 2 should also be supported in regtest",
addr: "2MwbFpRpZWv4zREjbdLB9jVW3Q8xonpVeyE",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// it should be a P2SH address
addr, err := DecodeBtcAddress(tt.addr, tt.chainId)
require.NoError(t, err)
_, ok := addr.(*btcutil.AddressScriptHash)
require.True(t, ok)

// it should be supported
supported := IsBtcAddressSupported(addr)
require.Equal(t, tt.supported, supported)
})
}
}

func Test_IsBtcAddressSupported_P2PKH(t *testing.T) {
tests := []struct {
name string
addr string
chainId int64
supported bool
}{
{
// https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca
name: "mainnet P2PKH address 1",
addr: "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte",
chainId: BtcMainnetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/1e3974386f071de7f65cabb57346c1a22ec9b3e211a96928a98149673f681237
name: "testnet P2PKH address 1",
addr: "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
// https://mempool.space/testnet/tx/e48459f372727f2253b0ea8c71ded83e8270873b8a044feb3435fc7a799a648f
name: "testnet P2PKH address 2",
addr: "n1gXcqxmzwqHmqmgobe1XXuJaweSu69tZz",
chainId: BtcTestNetChain().ChainId,
supported: true,
},
{
name: "testnet P2PKH address should also be supported in regtest",
addr: "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
{
name: "testnet P2PKH address should also be supported in regtest",
addr: "n1gXcqxmzwqHmqmgobe1XXuJaweSu69tZz",
chainId: BtcRegtestChain().ChainId,
supported: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// it should be a P2PKH address
addr, err := DecodeBtcAddress(tt.addr, tt.chainId)
require.NoError(t, err)
_, ok := addr.(*btcutil.AddressPubKeyHash)
require.True(t, ok)

// it should be supported
supported := IsBtcAddressSupported(addr)
require.Equal(t, tt.supported, supported)
})
}
}
23 changes: 15 additions & 8 deletions common/bitcoin/address_taproot.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
)

Expand All @@ -23,7 +24,7 @@ type AddressTaproot struct {
AddressSegWit
}

var _ btcutil.Address = AddressTaproot{}
var _ btcutil.Address = &AddressTaproot{}

// NewAddressTaproot returns a new AddressTaproot.
func NewAddressTaproot(witnessProg []byte,
Expand Down Expand Up @@ -171,15 +172,15 @@ func decodeSegWitAddress(address string) (string, byte, []byte, error) {
// ScriptAddress returns the witness program for this address.
//
// NOTE: This method is part of the Address interface.
func (a AddressSegWit) ScriptAddress() []byte {
func (a *AddressSegWit) ScriptAddress() []byte {
return a.witnessProgram[:]
}

// 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 {
func (a *AddressSegWit) IsForNet(net *chaincfg.Params) bool {
return a.hrp == net.Bech32HRPSegwit
}

Expand All @@ -188,23 +189,29 @@ func (a AddressSegWit) IsForNet(net *chaincfg.Params) bool {
// can be used as a fmt.Stringer.
//
// NOTE: This method is part of the Address interface.
func (a AddressSegWit) String() string {
func (a *AddressSegWit) String() string {
return a.EncodeAddress()
}

func DecodeTaprootAddress(addr string) (AddressTaproot, error) {
func DecodeTaprootAddress(addr string) (*AddressTaproot, error) {
hrp, version, program, err := decodeSegWitAddress(addr)
if err != nil {
return AddressTaproot{}, err
return nil, err
}
if version != 1 {
return AddressTaproot{}, errors.New("invalid witness version; taproot address must be version 1")
return nil, errors.New("invalid witness version; taproot address must be version 1")
}
return AddressTaproot{
return &AddressTaproot{
AddressSegWit{
hrp: hrp,
witnessVersion: version,
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()
}
Loading

0 comments on commit c2dfb7a

Please sign in to comment.