diff --git a/Gopkg.lock b/Gopkg.lock index 3ba26cfe..15c4e21b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -78,8 +78,8 @@ [[projects]] name = "github.com/decred/dcrd" packages = ["chaincfg","chaincfg/chainec","chaincfg/chainhash","dcrec","dcrec/edwards","dcrec/secp256k1","dcrec/secp256k1/schnorr","dcrjson","dcrutil","txscript","wire"] - revision = "0fe564967f03160b9dbe0a147420d8aa13371d12" - version = "v1.3.0" + revision = "e3e8c47c68b010dbddeb783ebad32a3a4993dd71" + version = "v1.4.0" [[projects]] name = "github.com/decred/slog" diff --git a/bchain/coins/dcr/decredparser.go b/bchain/coins/dcr/decredparser.go index a50611d1..8174a9e0 100644 --- a/bchain/coins/dcr/decredparser.go +++ b/bchain/coins/dcr/decredparser.go @@ -2,18 +2,24 @@ package dcr import ( "bytes" + "encoding/binary" "encoding/hex" "encoding/json" "math" "math/big" + "strconv" "blockbook/bchain" "blockbook/bchain/coins/btc" "blockbook/bchain/coins/utils" cfg "github.com/decred/dcrd/chaincfg" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/hdkeychain" "github.com/decred/dcrd/txscript" + "github.com/juju/errors" "github.com/martinboehm/btcd/wire" + "github.com/martinboehm/btcutil/base58" "github.com/martinboehm/btcutil/chaincfg" ) @@ -47,14 +53,23 @@ func init() { type DecredParser struct { *btc.BitcoinParser baseParser *bchain.BaseParser + netConfig *cfg.Params } // NewDecredParser returns new DecredParser instance func NewDecredParser(params *chaincfg.Params, c *btc.Configuration) *DecredParser { - return &DecredParser{ + d := &DecredParser{ BitcoinParser: btc.NewBitcoinParser(params, c), baseParser: &bchain.BaseParser{}, } + + switch d.BitcoinParser.Params.Name { + case "testnet3": + d.netConfig = &cfg.TestNet3Params + default: + d.netConfig = &cfg.MainNetParams + } + return d } // GetChainParams contains network parameters for the main Decred network, @@ -168,30 +183,25 @@ func (p *DecredParser) ParseTxFromJson(jsonTx json.RawMessage) (*bchain.Tx, erro return tx, nil } -// GetAddrDescForUnknownInput returns nil AddressDescriptor +// GetAddrDescForUnknownInput returns nil AddressDescriptor. func (p *DecredParser) GetAddrDescForUnknownInput(tx *bchain.Tx, input int) bchain.AddressDescriptor { return nil } +// GetAddrDescFromAddress returns internal address representation of a given address. func (p *DecredParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) { addressByte := []byte(address) return bchain.AddressDescriptor(addressByte), nil } +// GetAddrDescFromVout returns internal address representation of a given transaction output. func (p *DecredParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) { script, err := hex.DecodeString(output.ScriptPubKey.Hex) if err != nil { return nil, err } - var params cfg.Params - if p.Params.Name == "mainnet" { - params = cfg.MainNetParams - } else { - params = cfg.TestNet3Params - } - - scriptClass, addresses, _, err := txscript.ExtractPkScriptAddrs(txscript.DefaultScriptVersion, script, ¶ms) + scriptClass, addresses, _, err := txscript.ExtractPkScriptAddrs(txscript.DefaultScriptVersion, script, p.netConfig) if err != nil { return nil, err } @@ -206,17 +216,15 @@ func (p *DecredParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressD for i := range addresses { addressByte = append(addressByte, addresses[i].String()...) } - return bchain.AddressDescriptor(addressByte), nil } +// GetAddressesFromAddrDesc returns addresses obtained from the internal address representation func (p *DecredParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) { var addrs []string - if addrDesc != nil { addrs = append(addrs, string(addrDesc)) } - return addrs, true, nil } @@ -229,3 +237,119 @@ func (p *DecredParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([] func (p *DecredParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { return p.baseParser.UnpackTx(buf) } + +func (p *DecredParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchain.AddressDescriptor, error) { + var addr, err = extKey.Address(p.netConfig) + if err != nil { + return nil, err + } + return p.GetAddrDescFromAddress(addr.String()) +} + +// DeriveAddressDescriptors derives address descriptors from given xpub for +// listed indexes +func (p *DecredParser) DeriveAddressDescriptors(xpub string, change uint32, + indexes []uint32) ([]bchain.AddressDescriptor, error) { + 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, len(indexes)) + for i, index := range indexes { + indexExtKey, err := changeExtKey.Child(index) + if err != nil { + return nil, err + } + ad[i], err = p.addrDescFromExtKey(indexExtKey) + if err != nil { + return nil, err + } + } + return ad, nil +} + +// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for +// addresses in index range +func (p *DecredParser) DeriveAddressDescriptorsFromTo(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 +} + +// DerivationBasePath returns base path of xpub which whose full format is +// m/44'/'/'//
. This function only +// returns a path up to m/44'/'/'/ whereby the rest of the +// other details (/
) are populated automatically. +func (p *DecredParser) DerivationBasePath(xpub string) (string, error) { + var c string + cn, depth, err := p.decodeXpub(xpub) + if err != nil { + return "", err + } + + if cn >= hdkeychain.HardenedKeyStart { + cn -= hdkeychain.HardenedKeyStart + c = "'" + } + + c = strconv.Itoa(int(cn)) + c + if depth != 3 { + return "unknown/" + c, nil + } + + return "m/44'/" + strconv.Itoa(int(p.Slip44)) + "'/" + c, nil +} + +func (p *DecredParser) decodeXpub(xpub string) (childNum uint32, depth uint16, err error) { + decoded := base58.Decode(xpub) + + // serializedKeyLen is the length of a serialized public or private + // extended key. It consists of 4 bytes version, 1 byte depth, 4 bytes + // fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes + // public/private key data. + serializedKeyLen := 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes + if len(decoded) != serializedKeyLen+4 { + err = errors.New("invalid extended key length") + return + } + + payload := decoded[:len(decoded)-4] + checkSum := decoded[len(decoded)-4:] + expectedCheckSum := chainhash.HashB(chainhash.HashB(payload))[:4] + if !bytes.Equal(checkSum, expectedCheckSum) { + err = errors.New("bad checksum value") + return + } + + depth = uint16(payload[4:5][0]) + childNum = binary.BigEndian.Uint32(payload[9:13]) + return +} diff --git a/bchain/coins/dcr/decredparser_test.go b/bchain/coins/dcr/decredparser_test.go index cdd887a1..37f3f606 100644 --- a/bchain/coins/dcr/decredparser_test.go +++ b/bchain/coins/dcr/decredparser_test.go @@ -13,7 +13,7 @@ import ( ) var ( - parser *DecredParser + testnetParser, mainnetParser *DecredParser testTx1 = bchain.Tx{ Hex: "01000000012372568fe80d2f9b2ab17226158dd5732d9926dc705371eaf40ab748c9e3d9720200000001ffffffff02644b252d0000000000001976a914a862f83733cc368f386a651e03d844a5bd6116d588acacdf63090000000000001976a91491dc5d18370939b3414603a0729bcb3a38e4ef7688ac000000000000000001e48d893600000000bb3d0000020000006a4730440220378e1442cc17fa7e49184518713eedd30e13e42147e077859557da6ffbbd40c702205f85563c28b6287f9c9110e6864dd18acfd92d85509ea846913c28b6e8a7f940012102bbbd7aadef33f2d2bdd9b0c5ba278815f5d66a6a01d2c019fb73f697662038b5", @@ -54,6 +54,47 @@ var ( } testTx2 = bchain.Tx{ + Hex: "0100000001193c189c71dff482b70ccb10ec9cf0ea3421a7fc51e4c7b0cf59c98a293a2f960200000000ffffffff027c87f00b0000000000001976a91418f10131a859912119c4a8510199f87f0a4cec2488ac9889495f0000000000001976a914631fb783b1e06c3f6e71777e16da6de13450465e88ac0000000000000000015ced3d6b0000000030740000000000006a47304402204e6afc21f6d065b9c082dad81a5f29136320e2b54c6cdf6b8722e4507e1a8d8902203933c5e592df3b0bbb0568f121f48ef6cbfae9cf479a57229742b5780dedc57a012103b89bb443b6ab17724458285b302291b082c59e5a022f273af0f61d47a414a537", + Txid: "7058766ffef2e9cee61ee4b7604a39bc91c3000cb951c4f93f3307f6e0bf4def", + Blocktime: 1463843967, + Time: 1463843967, + LockTime: 0, + Version: 1, + Vin: []bchain.Vin{ + { + Txid: "962f3a298ac959cfb0c7e451fca72134eaf09cec10cb0cb782f4df719c183c19", + Vout: 2, + Sequence: 4294967295, + ScriptSig: bchain.ScriptSig{ + Hex: "47304402204e6afc21f6d065b9c082dad81a5f29136320e2b54c6cdf6b8722e4507e1a8d8902203933c5e592df3b0bbb0568f121f48ef6cbfae9cf479a57229742b5780dedc57a012103b89bb443b6ab17724458285b302291b082c59e5a022f273af0f61d47a414a537", + }, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(200312700), + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "76a91418f10131a859912119c4a8510199f87f0a4cec2488ac", + Addresses: []string{ + "DsTEnRLDEjQNeQ4A47fdS2pqtaFrGNzkqNa", + }, + }, + }, + { + ValueSat: *big.NewInt(1598654872), + N: 1, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "76a914631fb783b1e06c3f6e71777e16da6de13450465e88ac", + Addresses: []string{ + "Dsa12P9VnCd55hTnUXpvGgFKSeGkFkzRvYb", + }, + }, + }, + }, + } + + testTx3 = bchain.Tx{ Hex: "0100000001c56d80756eaa7fc6e3542b29f596c60a9bcc959cf04d5f6e6b12749e241ece290200000001ffffffff02cf20b42d0000000000001976a9140799daa3cd36b44def220886802eb99e10c4a7c488ac0c25c7070000000000001976a9140b102deb3314213164cb6322211225365658407e88ac000000000000000001afa87b3500000000e33d0000000000006a47304402201ff342e5aa55b6030171f85729221ca0b81938826cc09449b77752e6e3b615be0220281e160b618e57326b95a0e0c3ac7a513bd041aba63cbace2f71919e111cfdba01210290a8de6665c8caac2bb8ca1aabd3dc09a334f997f97bd894772b1e51cab003d9", Blocktime: 1535638326, Time: 1535638326, @@ -93,7 +134,8 @@ var ( ) func TestMain(m *testing.M) { - parser = NewDecredParser(GetChainParams("testnet3"), &btc.Configuration{}) + testnetParser = NewDecredParser(GetChainParams("testnet3"), &btc.Configuration{Slip44: 1}) + mainnetParser = NewDecredParser(GetChainParams("mainnet"), &btc.Configuration{Slip44: 42}) exitCode := m.Run() os.Exit(exitCode) } @@ -130,7 +172,7 @@ func TestGetAddrDescFromAddress(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parser.GetAddrDescFromAddress(tt.args.address) + got, err := testnetParser.GetAddrDescFromAddress(tt.args.address) if (err != nil) != tt.wantErr { t.Fatalf("GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) } @@ -174,7 +216,7 @@ func TestGetAddrDescFromVout(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parser.GetAddrDescFromVout(&tt.args.vout) + got, err := testnetParser.GetAddrDescFromVout(&tt.args.vout) if (err != nil) != tt.wantErr { t.Fatalf("GetAddrDescFromVout() error = %v, wantErr %v", err, tt.wantErr) } @@ -223,7 +265,7 @@ func TestGetAddressesFromAddrDesc(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b, _ := hex.DecodeString(tt.args.script) - got, got2, err := parser.GetAddressesFromAddrDesc(b) + got, got2, err := testnetParser.GetAddressesFromAddrDesc(b) if (err != nil) != tt.wantErr { t.Fatalf("GetAddressesFromAddrDesc() error = %v, wantErr %v", err, tt.wantErr) } @@ -236,3 +278,259 @@ func TestGetAddressesFromAddrDesc(t *testing.T) { }) } } + +func TestDeriveAddressDescriptors(t *testing.T) { + type args struct { + xpub string + change uint32 + indexes []uint32 + parser *DecredParser + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "m/44'/42'/0'", + args: args{ + xpub: "dpubZFYFpu8cZxwrApmtot59LZLChk5JcdB8xCxVQ4pcsTig4fscH3EfAkhxcKKhXBQH6SGyYs2VDidoomA5qukTWMaHDkBsAtnpodAHm61ozbD", + change: 0, + indexes: []uint32{0, 5}, + parser: mainnetParser, + }, + want: []string{"DsUPx4NgAJzUQFRXnn2XZnWwEeQkQpwhqFD", "DsaT4kaGCeJU1Fef721J2DNt8UgcrmE2UsD"}, + }, + { + name: "m/44'/42'/1'", + args: args{ + xpub: "dpubZFYFpu8cZxwrESo75eazNjVHtC4nWJqL5aXxExZHKnyvZxKirkpypbgeJhVzhTdfnK2986DLjich4JQqcSaSyxu5KSoZ25KJ67j4mQJ9iqx", + change: 0, + indexes: []uint32{0, 5}, + parser: mainnetParser, + }, + want: []string{"DsX5px9k9XZKFNP2Z9kyZBbfHgecm1ftNz6", "Dshjbo35CSWwNo7xMgG7UM8AWykwEjJ5DCP"}, + }, + { + name: "m/44'/1'/0'", + args: args{ + xpub: "tpubVossdTiJthe9xZZ5rz47szxN6ncpLJ4XmtJS26hKciDUPtboikdwHKZPWfo4FWYuLRZ6MNkLjyPRKhxqjStBTV2BE1LCULznpqsFakkPfPr", + change: 0, + indexes: []uint32{0, 2}, + parser: testnetParser, + }, + want: []string{"TsboBwzpaH831s9J63XDcDx5GbKLcwv9ujo", "TsXrNt9nP3kBUM2Wr3rQGoPrpL7RMMSJyJH"}, + }, + { + name: "m/44'/1'/1'", + args: args{ + xpub: "tpubVossdTiJtheA1fQniKn9EN1JE1Eq1kBofaq2KwywrvuNhAk1KsEM7J2r8anhMJUmmcn9Wmoh73EctpW7Vxs3gS8cbF7N3m4zVjzuyvBj3qC", + change: 0, + indexes: []uint32{0, 3}, + parser: testnetParser, + }, + want: []string{"TsndBjzcwZVjoZEuqYKwiMbCJH9QpkEekg4", "TsbrkVdFciW3Lfh1W8qjwRY9uSbdiBmY4VP"}, + }, + } + 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.indexes) + if (err != nil) != tt.wantErr { + t.Errorf("DeriveAddressDescriptorsFromTo() 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("DeriveAddressDescriptorsFromTo() got incorrect address descriptor %v, error %v", ad, err) + return + } + gotAddresses[i] = aa[0] + } + if !reflect.DeepEqual(gotAddresses, tt.want) { + t.Errorf("DeriveAddressDescriptorsFromTo() = %v, want %v", gotAddresses, tt.want) + } + }) + } +} + +func TestDeriveAddressDescriptorsFromTo(t *testing.T) { + type args struct { + xpub string + change uint32 + fromIndex uint32 + toIndex uint32 + parser *DecredParser + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "m/44'/42'/2'", + args: args{ + xpub: "dpubZFYFpu8cZxwrGnWbdHmvsAcTaMve4W9EAUiSHzXp1c5hQvfeWgk7LxsE5LqopwfxV62CoB51fxw97YaNpdA3tdo4GHbLxtUzRmYcUtVPYUi", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: mainnetParser, + }, + want: []string{"Dshtd1N7pKw814wgWXUq5qFVC5ENQ9oSGK7"}, + }, + { + name: "m/44'/42'/1'", + args: args{ + xpub: "dpubZFYFpu8cZxwrESo75eazNjVHtC4nWJqL5aXxExZHKnyvZxKirkpypbgeJhVzhTdfnK2986DLjich4JQqcSaSyxu5KSoZ25KJ67j4mQJ9iqx", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: mainnetParser, + }, + want: []string{"DsX5px9k9XZKFNP2Z9kyZBbfHgecm1ftNz6"}, + }, + { + name: "m/44'/1'/2'", + args: args{ + xpub: "tpubVossdTiJtheA51AuNQZtqvKUbhM867Von8XBadxX3tRkDm71kyyi6U966jDPEw9RnQjNcQLwxYSnQ9kBjZxrxfmSbByRbz7D1PLjgAPmL42", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: testnetParser, + }, + want: []string{"TsSpo87rBG21PLvvbzFk2Ust2Dbyvjfn8pQ"}, + }, + { + name: "m/44'/1'/1'", + args: args{ + xpub: "tpubVossdTiJtheA1fQniKn9EN1JE1Eq1kBofaq2KwywrvuNhAk1KsEM7J2r8anhMJUmmcn9Wmoh73EctpW7Vxs3gS8cbF7N3m4zVjzuyvBj3qC", + change: 0, + fromIndex: 0, + toIndex: 5, + parser: testnetParser, + }, + want: []string{"TsndBjzcwZVjoZEuqYKwiMbCJH9QpkEekg4", "TshWHbnPAVCDARTcCfTEQyL9SzeHxxexX4J", "TspE6pMdC937UHHyfYJpTiKi6vPj5rVnWiG", + "TsbrkVdFciW3Lfh1W8qjwRY9uSbdiBmY4VP", "TsagMXjC4Xj6ckPEJh8f1RKHU4cEzTtdVW6"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex) + if (err != nil) != tt.wantErr { + t.Errorf("DeriveAddressDescriptorsFromTo() 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("DeriveAddressDescriptorsFromTo() got incorrect address descriptor %v, error %v", ad, err) + return + } + gotAddresses[i] = aa[0] + } + if !reflect.DeepEqual(gotAddresses, tt.want) { + t.Errorf("DeriveAddressDescriptorsFromTo() = %v, want %v", gotAddresses, tt.want) + } + }) + } +} + +func TestDerivationBasePath(t *testing.T) { + tests := []struct { + name string + xpub string + parser *DecredParser + }{ + { + name: "m/44'/42'/2'", + xpub: "dpubZFYFpu8cZxwrGnWbdHmvsAcTaMve4W9EAUiSHzXp1c5hQvfeWgk7LxsE5LqopwfxV62CoB51fxw97YaNpdA3tdo4GHbLxtUzRmYcUtVPYUi", + parser: mainnetParser, + }, + { + name: "m/44'/42'/1'", + xpub: "dpubZFYFpu8cZxwrESo75eazNjVHtC4nWJqL5aXxExZHKnyvZxKirkpypbgeJhVzhTdfnK2986DLjich4JQqcSaSyxu5KSoZ25KJ67j4mQJ9iqx", + parser: mainnetParser, + }, + { + name: "m/44'/1'/2'", + xpub: "tpubVossdTiJtheA51AuNQZtqvKUbhM867Von8XBadxX3tRkDm71kyyi6U966jDPEw9RnQjNcQLwxYSnQ9kBjZxrxfmSbByRbz7D1PLjgAPmL42", + parser: testnetParser, + }, + { + name: "m/44'/1'/1'", + xpub: "tpubVossdTiJtheA1fQniKn9EN1JE1Eq1kBofaq2KwywrvuNhAk1KsEM7J2r8anhMJUmmcn9Wmoh73EctpW7Vxs3gS8cbF7N3m4zVjzuyvBj3qC", + parser: testnetParser, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.parser.DerivationBasePath(tt.xpub) + if err != nil { + t.Errorf("DerivationBasePath() expected no error but got %v", err) + return + } + + if got != tt.name { + t.Errorf("DerivationBasePath() = %v, want %v", got, tt.name) + } + }) + } +} + +func TestPackAndUnpack(t *testing.T) { + tests := []struct { + name string + txInfo *bchain.Tx + height uint32 + parser *DecredParser + }{ + { + name: "Test_1", + txInfo: &testTx1, + height: 15819, + parser: testnetParser, + }, + { + name: "Test_2", + txInfo: &testTx2, + height: 300000, + parser: mainnetParser, + }, + { + name: "Test_3", + txInfo: &testTx3, + height: 15859, + parser: testnetParser, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + packedTx, err := tt.parser.PackTx(tt.txInfo, tt.height, tt.txInfo.Blocktime) + if err != nil { + t.Errorf("PackTx() expected no error but got %v", err) + return + } + + unpackedtx, gotHeight, err := tt.parser.UnpackTx(packedTx) + if err != nil { + t.Errorf("PackTx() expected no error but got %v", err) + return + } + + if !reflect.DeepEqual(tt.txInfo, unpackedtx) { + t.Errorf("TestPackAndUnpack() expected the raw tx and the unpacked tx to match but they didn't") + } + + if gotHeight != tt.height { + t.Errorf("TestPackAndUnpack() = got height %v, but want %v", gotHeight, tt.height) + } + }) + } + +}