diff --git a/go.mod b/go.mod index 9d462fb..d711dbe 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,16 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.6 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/dchest/blake2b v1.0.0 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.43.0 ) require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.37.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7529568..8b7305e 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,7 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/sign.go b/sign.go index ffaf53f..d1cb1a7 100644 --- a/sign.go +++ b/sign.go @@ -49,6 +49,7 @@ var upgradeParams = []upgradeParam{ {1046400, []byte{0xA6, 0x75, 0xFF, 0xE9}}, // Canopy 0xe9ff75a6 {1687104, []byte{0xB4, 0xD0, 0xD6, 0xC2}}, // NU5 0xc2d6d0b4 {2726400, []byte{0x55, 0x10, 0xE7, 0xC8}}, // NU6 0xc8e71055 + {3146400, []byte{0xF0, 0x4D, 0xEC, 0x4D}}, // NU6.1 0x4dec4df0 } // RawTxInSignature returns the serialized ECDSA signature for the input idx of diff --git a/zecaddr.go b/zecaddr.go index 54b88df..8139b9f 100644 --- a/zecaddr.go +++ b/zecaddr.go @@ -3,9 +3,12 @@ package zecutil import ( "crypto/sha256" "errors" + "strings" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/base58" + bech32 "github.com/btcsuite/btcd/btcutil/bech32" // tex address "github.com/btcsuite/btcd/chaincfg" "golang.org/x/crypto/ripemd160" ) @@ -92,6 +95,11 @@ func DecodeAddress(address string, netName string) (btcutil.Address, error) { return nil, errors.New("unknown net") } + // add tex address support + if strings.HasPrefix(address, "tex1") || strings.HasPrefix(address, "textest1") { + return decodeTexAddress(address, netName) + } + var decoded = base58.Decode(address) if len(decoded) != 26 { return nil, base58.ErrInvalidFormat @@ -122,6 +130,46 @@ func DecodeAddress(address string, netName string) (btcutil.Address, error) { return nil, errors.New("unknown address") } +// decode tex/textest address +func decodeTexAddress(address string, netName string) (btcutil.Address, error) { + // 1. bech32 decode + hrp, data, ver, err := bech32.DecodeGeneric(address) + if err != nil { + return nil, err + } + + // 2. must be bech32m + if ver != bech32.VersionM { + return nil, errors.New("tex address must use bech32m") + } + + // 3. verify hrp: mainnet = tex testnet = textest + expectedHRP := "tex" + if strings.Contains(netName, "test") { + expectedHRP = "textest" + } + if hrp != expectedHRP { + return nil, errors.New("invalid tex hrp") + } + + // 4. 5bit words -> 8bit bytes(20-byte pkHash) + pkh, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + if len(pkh) != ripemd160.Size { + return nil, errors.New("invalid tex payload length") + } + + // 5. construct ZecAddressPubKeyHash + addr := &ZecAddressPubKeyHash{prefix: netName} + copy(addr.hash[:], pkh) + + return addr, nil +} + + + // EncodeAddress returns the string encoding of a pay-to-pubkey-hash // address. Part of the Address interface. func (a *ZecAddressPubKeyHash) EncodeAddress() (addr string) { @@ -192,3 +240,32 @@ func addrChecksum(input []byte) (cksum [4]byte) { return } + +// PkHashFromAddress parses a zcash address (t1/t3/tex/textest) and returns +// the 20-byte hash160 payload (pkHash/scriptHash), reusing DecodeAddress. +func PkHashFromAddress(address string, net *chaincfg.Params) ([]byte, error) { + if net == nil { + return nil, errors.New("nil net params") + } + + addr, err := DecodeAddress(address, net.Name) + if err != nil { + return nil, err + } + + switch a := addr.(type) { + case *ZecAddressPubKeyHash: + return a.ScriptAddress(), nil + case *ZecAddressScriptHash: + return a.ScriptAddress(), nil + default: + + type scriptAddresser interface { + ScriptAddress() []byte + } + if sa, ok := addr.(scriptAddresser); ok { + return sa.ScriptAddress(), nil + } + return nil, errors.New("unsupported address type for pkHash") + } +} diff --git a/zecaddr_test.go b/zecaddr_test.go index 699bc20..13bc9c9 100644 --- a/zecaddr_test.go +++ b/zecaddr_test.go @@ -1,9 +1,12 @@ package zecutil import ( - "github.com/btcsuite/btcd/btcutil" + "reflect" "testing" + "github.com/btcsuite/btcd/btcutil" + "github.com/stretchr/testify/require" + "github.com/btcsuite/btcd/chaincfg" ) @@ -61,3 +64,68 @@ func TestDecode(t *testing.T) { } } } + +func TestDecodeTexAndT1SameHash(t *testing.T) { + t1 := "t1XtsHnj4Ev6CWC3HfJ7Xu3GkEP7SCy8hxV" + tex := "tex1n88w7cmg9uzdluuct3krjqlkcxyz8tku8sq40s" + + addr1, err := DecodeAddress(t1, "mainnet") + require.NoError(t, err) + + addr2, err := DecodeAddress(tex, "mainnet") + require.NoError(t, err) + + a1 := addr1.(*ZecAddressPubKeyHash) + a2 := addr2.(*ZecAddressPubKeyHash) + + require.Equal(t, a1.hash, a2.hash) +} + + +func TestPkHashFromAddress_T1AndTexSame(t *testing.T) { + //(mainnet) + t1 := "t1XtsHnj4Ev6CWC3HfJ7Xu3GkEP7SCy8hxV" + tex := "tex1n88w7cmg9uzdluuct3krjqlkcxyz8tku8sq40s" + + netParam := &chaincfg.Params{ + Name: "mainnet", + } + + // 1. use PkHashFromAddress for pkHash + pkhT1, err := PkHashFromAddress(t1, netParam) + if err != nil { + t.Fatalf("PkHashFromAddress(t1) error: %v", err) + } + + pkhTex, err := PkHashFromAddress(tex, netParam) + if err != nil { + t.Fatalf("PkHashFromAddress(tex) error: %v", err) + } + + if !reflect.DeepEqual(pkhT1, pkhTex) { + t.Fatalf("pkHash not equal: t1=%x tex=%x", pkhT1, pkhTex) + } + + // 2. use DecodeAddress to check ScriptAddress + addrT1, err := DecodeAddress(t1, netParam.Name) + if err != nil { + t.Fatalf("DecodeAddress(t1) error: %v", err) + } + addrTex, err := DecodeAddress(tex, netParam.Name) + if err != nil { + t.Fatalf("DecodeAddress(tex) error: %v", err) + } + + z1, ok1 := addrT1.(*ZecAddressPubKeyHash) + if !ok1 { + t.Fatalf("DecodeAddress(t1) type = %T, want *ZecAddressPubKeyHash", addrT1) + } + z2, ok2 := addrTex.(*ZecAddressPubKeyHash) + if !ok2 { + t.Fatalf("DecodeAddress(tex) type = %T, want *ZecAddressPubKeyHash", addrTex) + } + + if !reflect.DeepEqual(z1.ScriptAddress(), z2.ScriptAddress()) { + t.Fatalf("ScriptAddress not equal: t1=%x tex=%x", z1.ScriptAddress(), z2.ScriptAddress()) + } +}