decred.org/dcrdex@v1.0.3/dex/networks/btc/descriptors_test.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package btc 5 6 import ( 7 "bytes" 8 "encoding/hex" 9 "reflect" 10 "testing" 11 12 "github.com/btcsuite/btcd/btcec/v2" 13 "github.com/btcsuite/btcd/btcutil" 14 "github.com/btcsuite/btcd/chaincfg" 15 "github.com/davecgh/go-spew/spew" 16 ) 17 18 var chainParams = &chaincfg.RegressionNetParams 19 20 func TestParseDescriptor(t *testing.T) { 21 tests := []struct { 22 name string 23 desc string 24 want *Descriptor 25 wantErr bool 26 }{ 27 { 28 "wpkh with bare key", 29 "wpkh([8a94b43c]039e9e0813e46041e2fddf46640006f4e9ae5d4d6ab811d0d2a6b372d0b136ba8a)#eq3vqyes", 30 &Descriptor{ 31 Function: "wpkh", 32 KeyOrigin: &KeyOrigin{ 33 Fingerprint: "8a94b43c", // fingerprint of corresponding private key itself 34 Path: "", 35 Steps: nil, 36 }, 37 Key: "039e9e0813e46041e2fddf46640006f4e9ae5d4d6ab811d0d2a6b372d0b136ba8a", 38 KeyFmt: KeyHexPub, 39 Nested: nil, 40 Expression: "[8a94b43c]039e9e0813e46041e2fddf46640006f4e9ae5d4d6ab811d0d2a6b372d0b136ba8a", 41 Checksum: "eq3vqyes", 42 }, 43 false, 44 }, { 45 "wpkh with origin and checksum", 46 "wpkh([b940190e/84'/1'/0'/0/0]030003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2)#0pfw7rck", 47 &Descriptor{ 48 Function: "wpkh", 49 KeyOrigin: &KeyOrigin{ 50 Fingerprint: "b940190e", 51 Path: "84'/1'/0'/0/0", 52 Steps: []uint32{2147483732, 2147483649, 2147483648, 0, 0}, 53 }, 54 Key: "030003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2", 55 KeyFmt: KeyHexPub, 56 Nested: nil, 57 Expression: "[b940190e/84'/1'/0'/0/0]030003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2", 58 Checksum: "0pfw7rck", 59 }, 60 false, 61 }, { 62 "range/extended wpkh-in-sh with origin and checksum", 63 "sh(wpkh([b940190e/49'/1'/0']tpubDCDYiBwbWWM3FRB55DcdgWyr7AVraCmXSgnVHZpyJ716tWigvdhShXGgAREnQwXjBqvnuaT7k1oHA5LD2HN5uPjp1u4ubAemppGmqioFHAq/1/*))#a73wy5hk", 64 &Descriptor{ 65 Function: "sh", 66 KeyOrigin: &KeyOrigin{ // dup of nested 67 Fingerprint: "b940190e", 68 Path: "49'/1'/0'", 69 Steps: []uint32{2147483697, 2147483649, 2147483648}, 70 }, 71 Key: "tpubDCDYiBwbWWM3FRB55DcdgWyr7AVraCmXSgnVHZpyJ716tWigvdhShXGgAREnQwXjBqvnuaT7k1oHA5LD2HN5uPjp1u4ubAemppGmqioFHAq/1/*", // dup of nested 72 KeyFmt: KeyExtended, // dup of nested 73 Nested: &Descriptor{ 74 Function: "wpkh", 75 KeyOrigin: &KeyOrigin{ 76 Fingerprint: "b940190e", 77 Path: "49'/1'/0'", 78 Steps: []uint32{2147483697, 2147483649, 2147483648}, 79 }, 80 Key: "tpubDCDYiBwbWWM3FRB55DcdgWyr7AVraCmXSgnVHZpyJ716tWigvdhShXGgAREnQwXjBqvnuaT7k1oHA5LD2HN5uPjp1u4ubAemppGmqioFHAq/1/*", 81 KeyFmt: KeyExtended, 82 Nested: nil, 83 Expression: "[b940190e/49'/1'/0']tpubDCDYiBwbWWM3FRB55DcdgWyr7AVraCmXSgnVHZpyJ716tWigvdhShXGgAREnQwXjBqvnuaT7k1oHA5LD2HN5uPjp1u4ubAemppGmqioFHAq/1/*", 84 Checksum: "", 85 }, 86 Expression: "wpkh([b940190e/49'/1'/0']tpubDCDYiBwbWWM3FRB55DcdgWyr7AVraCmXSgnVHZpyJ716tWigvdhShXGgAREnQwXjBqvnuaT7k1oHA5LD2HN5uPjp1u4ubAemppGmqioFHAq/1/*)", 87 Checksum: "a73wy5hk", 88 }, 89 false, 90 }, { 91 "range private wpkh master (no key origin)", 92 "wpkh(tprv8ZgxMBicQKsPdAQ2QZeTReB2hH2aKXBWGqgnrW1aYbutbC7YfUtPPJm1Nppb6eXy5hnLRrRqwCctBecfZV8HLNsLeivVhKT1BYFBiRbhUES/84'/1'/0'/0/*)#pm06dltl", 93 &Descriptor{ 94 Function: "wpkh", 95 KeyOrigin: nil, 96 Key: "tprv8ZgxMBicQKsPdAQ2QZeTReB2hH2aKXBWGqgnrW1aYbutbC7YfUtPPJm1Nppb6eXy5hnLRrRqwCctBecfZV8HLNsLeivVhKT1BYFBiRbhUES/84'/1'/0'/0/*", 97 KeyFmt: KeyExtended, 98 Nested: nil, 99 Expression: "tprv8ZgxMBicQKsPdAQ2QZeTReB2hH2aKXBWGqgnrW1aYbutbC7YfUtPPJm1Nppb6eXy5hnLRrRqwCctBecfZV8HLNsLeivVhKT1BYFBiRbhUES/84'/1'/0'/0/*", 100 Checksum: "pm06dltl", 101 }, 102 false, 103 }, { 104 "invalid nested sh", 105 "wsh(sh(abababab))", 106 nil, 107 true, 108 }, { 109 "invalid nested wsh", 110 "wsh(wsh(abababab))", 111 nil, 112 true, 113 }, 114 { 115 "valid nested wsh", 116 "sh(wsh(pk(03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc)))", 117 &Descriptor{ 118 Function: "sh", 119 KeyOrigin: nil, 120 Key: "03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc", // dup of nested 121 KeyFmt: KeyHexPub, // dup of nested 122 Nested: &Descriptor{ 123 Function: "wsh", 124 KeyOrigin: nil, 125 Key: "03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc", 126 KeyFmt: KeyHexPub, 127 Nested: &Descriptor{ 128 Function: "pk", 129 KeyOrigin: nil, 130 Key: "03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc", 131 KeyFmt: KeyHexPub, 132 Nested: nil, 133 Expression: "03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc", 134 Checksum: "", 135 }, 136 Expression: "pk(03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc)", 137 Checksum: "", 138 }, 139 Expression: "wsh(pk(03a40e1db1a51231027a8261b15317a59218b6c1c5dbd3d155688a3fb32c547ccc))", 140 Checksum: "", 141 }, 142 false, 143 }, 144 } 145 for _, tt := range tests { 146 t.Run(tt.name, func(t *testing.T) { 147 got, err := ParseDescriptor(tt.desc) 148 if (err != nil) != tt.wantErr { 149 t.Errorf("ParseDescriptor() error = %v, wantErr %v", err, tt.wantErr) 150 return 151 } 152 153 if !reflect.DeepEqual(got, tt.want) { 154 t.Errorf("ParseDescriptor() = %#v, want %#v", got, tt.want) 155 spew.Dump(got) 156 } 157 }) 158 } 159 } 160 161 func TestDescriptors(t *testing.T) { 162 // getaddressinfo bcrt1qwuqqg9dajf7f7ddxp4un3p2dtagvrny4qll6xe 163 descAddr := "wpkh([b940190e/84'/1'/0'/0/0]030003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2)#0pfw7rck" 164 d, err := ParseDescriptor(descAddr) 165 if err != nil { 166 t.Fatal(err) 167 } 168 if d.KeyFmt != KeyHexPub { 169 t.Fatalf("wrong key type: %v", d.Key) 170 } 171 172 if d.KeyOrigin == nil { 173 t.Fatalf("no key origin section") 174 } 175 if d.KeyOrigin.Fingerprint != "b940190e" { 176 t.Fatalf("incorrect key origin master fingerprint: %v", d.KeyOrigin.Fingerprint) 177 } 178 if d.KeyOrigin.Path != "84'/1'/0'/0/0" { 179 t.Fatalf("wrong path %q", d.KeyOrigin.Path) 180 } 181 if len(d.KeyOrigin.Steps) != 5 { 182 t.Fatalf("wrong number of steps in path: %d", len(d.KeyOrigin.Steps)) 183 } 184 addrPath := d.KeyOrigin.Steps 185 addrOriginFP := d.KeyOrigin.Fingerprint 186 187 addrPubKey, err := hex.DecodeString(d.Key) 188 if err != nil { 189 t.Fatalf("DecodeString: %v", err) 190 } 191 if _, err = btcec.ParsePubKey(addrPubKey); err != nil { 192 t.Fatalf("ParsePubKey: %v", err) 193 } 194 addr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(addrPubKey), chainParams) 195 if err != nil { 196 t.Fatalf("NewAddressWitnessPubKeyHash: %v", err) 197 } 198 if addr.String() != "bcrt1qwuqqg9dajf7f7ddxp4un3p2dtagvrny4qll6xe" { 199 t.Fatal("wrong address pubkey") 200 } 201 202 // listdescriptors private=true 203 204 descPriv := "wpkh(tprv8ZgxMBicQKsPdAQ2QZeTReB2hH2aKXBWGqgnrW1aYbutbC7YfUtPPJm1Nppb6eXy5hnLRrRqwCctBecfZV8HLNsLeivVhKT1BYFBiRbhUES/84'/1'/0'/0/*)#pm06dltl" 205 dPriv, err := ParseDescriptor(descPriv) 206 if err != nil { 207 t.Fatal(err) 208 } 209 if dPriv.KeyFmt != KeyExtended { 210 t.Fatalf("wrong key type: %v", dPriv.Key) 211 } 212 if dPriv.KeyOrigin != nil { 213 t.Fatalf("unexpected key origin section") 214 } 215 216 xPriv, fingerprint, pathStr, isRange, err := ParseKeyExtended(dPriv.Key) 217 if err != nil { 218 t.Fatal(err) 219 } 220 if !xPriv.IsPrivate() { 221 t.Fatal("not an extended private key") 222 } 223 if !isRange { 224 t.Fatal("not a range path") 225 } 226 parentPath, isRange, err := ParsePath(pathStr) 227 if err != nil { 228 t.Fatal(err) 229 } 230 if isRange { 231 t.Fatal("cleaned path was a range path") 232 } 233 234 // addrPath: b940190e/84'/1'/0'/0/0 <-- child index at end 235 // parentPath: b940190e/84'/1'/0'/0 236 if len(addrPath) != len(parentPath)+1 { 237 t.Fatal("paths do not agree") 238 } 239 for i := range parentPath { 240 if addrPath[i] != parentPath[i] { 241 t.Errorf("path depth %d incorrect, want %d, got %d", i, addrPath[i], parentPath[i]) 242 } 243 } 244 childIdx := addrPath[len(addrPath)-1] 245 246 // fingerprint := hex.EncodeToString(keyFingerprint(xPriv)) // this is what we need to get for each entry in listdescriptors 247 if addrOriginFP != fingerprint { 248 t.Fatalf("wrong fingerprint") 249 } 250 251 // now that we have the corresponding master private key, derive the branch 252 // extended private key, then the child address key itself. 253 254 branch, err := DeepChild(xPriv, parentPath) 255 if err != nil { 256 t.Fatalf("DeepChild: %v", err) 257 } 258 259 child, _ := branch.Derive(childIdx) 260 pubkey := pubKeyBytes(child) 261 pkh := btcutil.Hash160(pubkey) 262 addrWPKH, err := btcutil.NewAddressWitnessPubKeyHash(pkh, chainParams) 263 if err != nil { 264 t.Fatalf("NewAddressWitnessPubKeyHash: %v", err) 265 } 266 if addrWPKH.String() != addr.String() { 267 t.Fatalf("wrong address %s", addrWPKH) 268 } 269 270 privkey, err := child.ECPrivKey() 271 if err != nil { 272 t.Fatalf("ECPrivKey: %v", err) 273 } 274 pubkey = privkey.PubKey().SerializeCompressed() 275 // check against "pubkey" field of getaddressinfo result 276 if !bytes.Equal(pubkey, addrPubKey) { 277 t.Fatal("wrong address pubkey") 278 } 279 } 280 281 func TestParsePath(t *testing.T) { 282 tests := []struct { 283 name string 284 path string 285 wantPath []uint32 286 wantIsRange bool 287 wantErr bool 288 }{ 289 { 290 "empty", 291 "", 292 nil, 293 false, 294 false, 295 }, { 296 "unprefixed", // like path from ParseDescriptor or ParseKeyExtended 297 "84'/1'/0'/0/0", 298 []uint32{2147483732, 2147483649, 2147483648, 0, 0}, 299 false, 300 false, 301 }, { 302 "m", // like "hdkeypath" in getaddressinfo response 303 "m/84'/1h/0'/0/0", 304 []uint32{2147483732, 2147483649, 2147483648, 0, 0}, 305 false, 306 false, 307 }, { 308 "/", 309 "/84'/1'/0'/0h/0", 310 []uint32{2147483732, 2147483649, 2147483648, 2147483648, 0}, 311 false, 312 false, 313 }, { 314 "range", 315 "m/84'/1'/0'/0/0/*", 316 []uint32{2147483732, 2147483649, 2147483648, 0, 0}, 317 true, 318 false, 319 }, 320 } 321 for _, tt := range tests { 322 t.Run(tt.name, func(t *testing.T) { 323 gotPath, gotIsRange, err := ParsePath(tt.path) 324 if (err != nil) != tt.wantErr { 325 t.Errorf("ParsePath() error = %v, wantErr %v", err, tt.wantErr) 326 return 327 } 328 if !reflect.DeepEqual(gotPath, tt.wantPath) { 329 t.Errorf("ParsePath() gotPath = %v, want %v", gotPath, tt.wantPath) 330 } 331 if gotIsRange != tt.wantIsRange { 332 t.Errorf("ParsePath() gotIsRange = %v, want %v", gotIsRange, tt.wantIsRange) 333 } 334 }) 335 } 336 } 337 338 func TestParseKeyExtended(t *testing.T) { 339 tests := []struct { 340 name string 341 keyStr string 342 wantFingerprint string 343 wantPath string 344 wantIsRange bool 345 wantErr bool 346 }{ 347 { 348 "tprv with ranged path", 349 "tprv8ZgxMBicQKsPdAQ2QZeTReB2hH2aKXBWGqgnrW1aYbutbC7YfUtPPJm1Nppb6eXy5hnLRrRqwCctBecfZV8HLNsLeivVhKT1BYFBiRbhUES/84'/1'/0'/0/*", 350 "b940190e", 351 "84'/1'/0'/0", 352 true, 353 false, 354 }, { 355 "just tprv", 356 "tprv8ZgxMBicQKsPdAQ2QZeTReB2hH2aKXBWGqgnrW1aYbutbC7YfUtPPJm1Nppb6eXy5hnLRrRqwCctBecfZV8HLNsLeivVhKT1BYFBiRbhUES", 357 "b940190e", 358 "", 359 false, 360 false, 361 }, { 362 "full tpub", 363 "tpubDCoAK65iKNvE5x3wnCb87xRwD8wKDEKUyymu49KSgj9c5PG7DbfnYvwoPjgZaGhgTR4GfAQECPxrya46jeyiVn7jT1wuLDvb5CjJG6Q8FbT/0/*", 364 "af5360d5", 365 "0", 366 true, 367 false, 368 }, { 369 "hex pub", 370 "030003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2", 371 "", 372 "", 373 false, 374 true, 375 }, { 376 "wif priv", 377 "cSqGdZNwiMcqJqC1NCwigLtQWmooMNnz5jMVuzuLd4pRiLP7CgFM", // alpha harness addr key 378 "", 379 "", 380 false, 381 true, 382 }, 383 } 384 for _, tt := range tests { 385 t.Run(tt.name, func(t *testing.T) { 386 _, gotFingerprint, gotPath, gotIsRange, err := ParseKeyExtended(tt.keyStr) 387 if (err != nil) != tt.wantErr { 388 t.Errorf("ParseKeyExtended() error = %v, wantErr %v", err, tt.wantErr) 389 return 390 } 391 if tt.wantErr { 392 return 393 } 394 if gotFingerprint != tt.wantFingerprint { 395 t.Errorf("ParseKeyExtended() gotFingerprint = %v, want %v", gotFingerprint, tt.wantFingerprint) 396 } 397 if gotPath != tt.wantPath { 398 t.Errorf("ParseKeyExtended() gotPath = %v, want %v", gotPath, tt.wantPath) 399 } 400 if gotIsRange != tt.wantIsRange { 401 t.Errorf("ParseKeyExtended() gotIsRange = %v, want %v", gotIsRange, tt.wantIsRange) 402 } 403 }) 404 } 405 } 406 407 func Test_checkDescriptorKey(t *testing.T) { 408 tests := []struct { 409 name string 410 key string 411 want KeyFmt 412 }{ 413 { 414 "hex pub (compressed)", 415 "030003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2", 416 KeyHexPub, 417 }, { 418 "hex pub (uncompressed)", 419 "040003429cd5d23b1a229ec88dba6f2b69fb539fe26cd80229267aa0c992dc26b2cd3d19f0341e9064e21400bcde458ec96c38c25924413440c47cf5358443e871", 420 KeyHexPub, 421 }, { 422 "wif priv", 423 "cSqGdZNwiMcqJqC1NCwigLtQWmooMNnz5jMVuzuLd4pRiLP7CgFM", 424 KeyWIFPriv, 425 }, { 426 "extended with path", 427 "tpubDCoAK65iKNvE5x3wnCb87xRwD8wKDEKUyymu49KSgj9c5PG7DbfnYvwoPjgZaGhgTR4GfAQECPxrya46jeyiVn7jT1wuLDvb5CjJG6Q8FbT/0/*", 428 KeyExtended, 429 }, { 430 "extended no path", 431 "tpubDCoAK65iKNvE5x3wnCb87xRwD8wKDEKUyymu49KSgj9c5PG7DbfnYvwoPjgZaGhgTR4GfAQECPxrya46jeyiVn7jT1wuLDvb5CjJG6Q8FbT", 432 KeyExtended, 433 }, { 434 "unknown", 435 "asdfsadfsadf", 436 KeyUnknown, 437 }, 438 } 439 for _, tt := range tests { 440 t.Run(tt.name, func(t *testing.T) { 441 if got := checkDescriptorKey(tt.key); got != tt.want { 442 t.Errorf("checkDescriptorKey() = %v, want %v", got, tt.want) 443 } 444 }) 445 } 446 }