From 986275bb76bbc61b4886c7ebd393df8b2b70549f Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Mon, 28 Jan 2019 18:29:12 +0100 Subject: [PATCH] Implement parser.DeriveAddressDescriptors from xpub --- Gopkg.lock | 4 +- bchain/baseparser.go | 5 ++ bchain/coins/btc/bitcoinparser.go | 61 +++++++++++++++++++++- bchain/coins/btc/bitcoinparser_test.go | 72 ++++++++++++++++++++++++++ bchain/coins/btc/bitcoinrpc.go | 3 ++ bchain/types.go | 2 + 6 files changed, 144 insertions(+), 3 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index f26472e8..d0bea5ee 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -94,8 +94,8 @@ [[projects]] branch = "master" name = "github.com/martinboehm/btcutil" - packages = [".","base58","bech32","chaincfg","txscript"] - revision = "613fec26904062ae125fb073762af3a77c77b6c7" + packages = [".","base58","bech32","chaincfg","hdkeychain","txscript"] + revision = "9b332d8046124a83bab2830696e8ebddaf3f1788" [[projects]] branch = "master" diff --git a/bchain/baseparser.go b/bchain/baseparser.go index d18da3b4..e62b28f7 100644 --- a/bchain/baseparser.go +++ b/bchain/baseparser.go @@ -267,6 +267,11 @@ func (p *BaseParser) UnpackTx(buf []byte) (*Tx, uint32, error) { return &tx, pt.Height, nil } +// DeriveAddressDescriptors is unsupported +func (p *BaseParser) DeriveAddressDescriptors(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) { + return nil, errors.New("Not supported") +} + // EthereumTypeGetErc20FromTx is unsupported func (p *BaseParser) EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) { return nil, errors.New("Not supported") diff --git a/bchain/coins/btc/bitcoinparser.go b/bchain/coins/btc/bitcoinparser.go index f3731e0a..15264fd3 100644 --- a/bchain/coins/btc/bitcoinparser.go +++ b/bchain/coins/btc/bitcoinparser.go @@ -8,10 +8,12 @@ import ( "math/big" vlq "github.com/bsm/go-vlq" + "github.com/juju/errors" "github.com/martinboehm/btcd/blockchain" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil" "github.com/martinboehm/btcutil/chaincfg" + "github.com/martinboehm/btcutil/hdkeychain" "github.com/martinboehm/btcutil/txscript" ) @@ -23,6 +25,9 @@ type BitcoinParser struct { *bchain.BaseParser Params *chaincfg.Params OutputScriptToAddressesFunc OutputScriptToAddressesFunc + XPubMagic uint32 + XPubMagicSegwitP2sh uint32 + XPubMagicSegwitNative uint32 } // NewBitcoinParser returns new BitcoinParser instance @@ -32,7 +37,10 @@ func NewBitcoinParser(params *chaincfg.Params, c *Configuration) *BitcoinParser BlockAddressesToKeep: c.BlockAddressesToKeep, AmountDecimalPoint: 8, }, - Params: params, + Params: params, + XPubMagic: c.XPubMagic, + XPubMagicSegwitP2sh: c.XPubMagicSegwitP2sh, + XPubMagicSegwitNative: c.XPubMagicSegwitNative, } p.OutputScriptToAddressesFunc = p.outputScriptToAddresses return p @@ -266,3 +274,54 @@ func (p *BitcoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { return tx, height, nil } + +func (p *BitcoinParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchain.AddressDescriptor, error) { + var a btcutil.Address + var err error + if extKey.Version() == p.XPubMagicSegwitP2sh { + // redeemScript <20-byte-pubKeyHash> + pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes()) + redeemScript := make([]byte, len(pubKeyHash)+2) + redeemScript[0] = 0 + redeemScript[1] = byte(len(pubKeyHash)) + copy(redeemScript[2:], pubKeyHash) + hash := btcutil.Hash160(redeemScript) + a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params) + } else if extKey.Version() == p.XPubMagicSegwitNative { + a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params) + } else { + // default to P2PKH address + a, err = extKey.Address(p.Params) + } + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(a) +} + +// DeriveAddressDescriptors derives address descriptors from given xpub +func (p *BitcoinParser) DeriveAddressDescriptors(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { + if toIndex <= fromIndex { + return nil, errors.New("toIndex<=fromIndex") + } + extKey, err := hdkeychain.NewKeyFromString(xpub) + if err != nil { + return nil, err + } + changeExtKey, err := extKey.Child(change) + if err != nil { + return nil, err + } + ad := make([]bchain.AddressDescriptor, toIndex-fromIndex) + for index := fromIndex; index < toIndex; index++ { + indexExtKey, err := changeExtKey.Child(index) + if err != nil { + return nil, err + } + ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey) + if err != nil { + return nil, err + } + } + return ad, nil +} diff --git a/bchain/coins/btc/bitcoinparser_test.go b/bchain/coins/btc/bitcoinparser_test.go index e3062215..8aefdda0 100644 --- a/bchain/coins/btc/bitcoinparser_test.go +++ b/bchain/coins/btc/bitcoinparser_test.go @@ -417,3 +417,75 @@ func Test_UnpackTx(t *testing.T) { }) } } + +func Test_DeriveAddressDescriptors(t *testing.T) { + btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) + type args struct { + xpub string + change uint32 + fromIndex uint32 + toIndex uint32 + parser *BitcoinParser + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "m/44'/0'/0'", + args: args{ + xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"}, + }, + { + name: "m/49'/0'/0'", + args: args{ + xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"}, + }, + { + name: "m/84'/0'/0'", + args: args{ + xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.DeriveAddressDescriptors(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex) + if (err != nil) != tt.wantErr { + t.Errorf("DeriveAddressDescriptors() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotAddresses := make([]string, len(got)) + for i, ad := range got { + aa, _, err := tt.args.parser.GetAddressesFromAddrDesc(ad) + if err != nil || len(aa) != 1 { + t.Errorf("DeriveAddressDescriptors() got incorrect address descriptor %v, error %v", ad, err) + return + } + gotAddresses[i] = aa[0] + } + if !reflect.DeepEqual(gotAddresses, tt.want) { + t.Errorf("DeriveAddressDescriptors() = %v, want %v", gotAddresses, tt.want) + } + }) + } +} diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index a2fc0f11..e9f9e4ec 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -51,6 +51,9 @@ type Configuration struct { AddressFormat string `json:"address_format"` SupportsEstimateFee bool `json:"supports_estimate_fee"` SupportsEstimateSmartFee bool `json:"supports_estimate_smart_fee"` + XPubMagic uint32 `json:"xpub_magic,omitempty"` + XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` + XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` } // NewBitcoinRPC returns new BitcoinRPC instance. diff --git a/bchain/types.go b/bchain/types.go index a643b9dd..9f44940e 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -263,6 +263,8 @@ type BlockChainParser interface { PackBlockHash(hash string) ([]byte, error) UnpackBlockHash(buf []byte) (string, error) ParseBlock(b []byte) (*Block, error) + // xpub + DeriveAddressDescriptors(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) // EthereumType specific EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) }