package wallet import ( "encoding/hex" "strings" "testing" "git.smesh.lol/tinywallet/pkg/bech32" "git.smesh.lol/tinywallet/pkg/mnemonic" ) // bip39Phrase is the reference phrase the BIP84 vectors use: twelve words of zero entropy. const bip39Phrase = "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about" // seedOf derives the reference seed for a phrase and passphrase. func seedOf(t *testing.T, phrase, passphrase string) [64]byte { t.Helper() seed, err := mnemonic.Seed(phrase, passphrase) if err != nil { t.Fatal(err) } return seed } // TestBIP84Vectors derives the three addresses the BIP84 specification publishes. // // It is the whole stack in one assertion: the BIP39 seed, BIP32 private derivation, the // secp256k1 public key, hash160 and the bech32 encoder. If any of them is wrong the address // is not this one, and the specification is the authority rather than this package's own // output. func TestBIP84Vectors(t *testing.T) { profile, err := ProfileFor("btc", "btc") if err != nil { t.Fatal(err) } w, err := New(seedOf(t, bip39Phrase, ""), profile) if err != nil { t.Fatal(err) } for _, test := range []struct { change bool index uint32 pubkey string address string }{ { change: false, index: 0, pubkey: "0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c", address: "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", }, { change: false, index: 1, pubkey: "03e775fd51f0dfb8cd865d9ff1cca2a158cf651fe997fdc9fee9c1d3b5e995ea77", address: "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g", }, { change: true, index: 0, pubkey: "03025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a6", address: "bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", }, } { address, err := w.Address(test.change, test.index) if err != nil { t.Fatalf("%v/%d: %v", test.change, test.index, err) } if hex.EncodeToString(address.Pubkey) != test.pubkey { t.Fatalf("%v/%d pubkey is %s, want %s", test.change, test.index, hex.EncodeToString(address.Pubkey), test.pubkey) } if address.Text != test.address { t.Fatalf("%v/%d address is %s, want %s", test.change, test.index, address.Text, test.address) } if address.Path.String() != pathString(test.change, test.index) { t.Fatalf("%v/%d path is %s", test.change, test.index, address.Path) } } } // pathString is the path the BIP84 vectors name. func pathString(change bool, index uint32) string { branch := "0" if change { branch = "1" } return "m/84'/0'/0'/" + branch + "/" + itoa(index) } // itoa renders a small index. func itoa(value uint32) string { if value == 0 { return "0" } var digits []byte for value > 0 { digits = append([]byte{byte('0' + value%10)}, digits...) value /= 10 } return string(digits) } // TestGeneratorAddress: the P2WPKH address of the secp256k1 generator point is the // canonical BIP173 example, so the address encoder is checked against something other than // the derivation that feeds it. func TestGeneratorAddress(t *testing.T) { pubkey, err := hex.DecodeString( "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") if err != nil { t.Fatal(err) } program := hash160(pubkey) address, err := bech32.EncodeSegwit("bc", 0, program[:]) if err != nil { t.Fatal(err) } if address != "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" { t.Fatalf("the generator address is %s", address) } } // TestTheTwoChainsDeriveDifferentKeys: one seed, two accounts, no shared key. // // This is the property the forked chain depends on: it has no registered coin type, so it // takes a second account under the same one, and a mistake here would put the same key on // two chains. func TestTheTwoChainsDeriveDifferentKeys(t *testing.T) { seed := seedOf(t, bip39Phrase, "") btcProfile, err := ProfileFor("btc", "regtest") if err != nil { t.Fatal(err) } blakeProfile, err := ProfileFor("blake", "regtest") if err != nil { t.Fatal(err) } btc, err := New(seed, btcProfile) if err != nil { t.Fatal(err) } blake, err := New(seed, blakeProfile) if err != nil { t.Fatal(err) } btcAddress, err := btc.Address(false, 0) if err != nil { t.Fatal(err) } blakeAddress, err := blake.Address(false, 0) if err != nil { t.Fatal(err) } if hex.EncodeToString(btcAddress.Pubkey) == hex.EncodeToString(blakeAddress.Pubkey) { t.Fatal("the two chains derived the same public key") } if btcAddress.Program == blakeAddress.Program { t.Fatal("the two chains derived the same witness program") } if btcAddress.Text == blakeAddress.Text { t.Fatal("the two chains derived the same address") } // Both chains are on the same network, so they share the coin type and differ only in // the account: that is what one seed, two wallets means. if btcAddress.Path.String() != "m/84'/1'/0'/0/0" { t.Fatalf("the btc path is %s", btcAddress.Path) } if blakeAddress.Path.String() != "m/84'/1'/1'/0/0" { t.Fatalf("the blake path is %s", blakeAddress.Path) } // The two profiles are still the same network family: only the account differs. if btcProfile.CoinType != blakeProfile.CoinType { t.Fatalf("the coin types differ: %d and %d", btcProfile.CoinType, blakeProfile.CoinType) } // Both regtest profiles carry the regtest prefix, which is the point: the fork is the // same network as Bitcoin, so the account is what separates the two wallets. if btcProfile.HRP != "bcrt" || blakeProfile.HRP != "bcrt" { t.Fatalf("the regtest prefixes are %q and %q", btcProfile.HRP, blakeProfile.HRP) } } // TestAddressIsChainScoped: an address carries the prefix of its chain, and decoding one // under another chain's prefix is a different string. func TestAddressIsChainScoped(t *testing.T) { profile, err := ProfileFor("btc", "btc") if err != nil { t.Fatal(err) } // The rig is regtest, so the prefix the wallet uses there is bcrt. profile.HRP = "bcrt" w, err := New(seedOf(t, bip39Phrase, ""), profile) if err != nil { t.Fatal(err) } address, err := w.Address(false, 0) if err != nil { t.Fatal(err) } if !strings.HasPrefix(address.Text, "bcrt1") { t.Fatalf("the regtest address is %s", address.Text) } hrp, version, program, err := bech32.DecodeSegwit(address.Text) if err != nil { t.Fatal(err) } if hrp != "bcrt" || version != 0 || program == nil { t.Fatalf("decoded to %q/%d/%x", hrp, version, program) } if address.Chain != "btc" { t.Fatalf("the address names chain %q", address.Chain) } // The same program under the wrong prefix is not the same address, which is what makes // showing a BTC address as an XBT one a real mistake rather than a cosmetic one. mainnet, err := bech32.EncodeSegwit("bc", 0, program) if err != nil { t.Fatal(err) } if mainnet == address.Text { t.Fatal("an address is identical under two prefixes") } } // TestScript: the filter client matches a script, not an address. func TestScript(t *testing.T) { profile, err := ProfileFor("btc", "btc") if err != nil { t.Fatal(err) } w, err := New(seedOf(t, bip39Phrase, ""), profile) if err != nil { t.Fatal(err) } address, err := w.Address(false, 0) if err != nil { t.Fatal(err) } script := address.Script() if len(script) != 22 { t.Fatalf("the script is %d bytes", len(script)) } if script[0] != 0x00 || script[1] != 0x14 { t.Fatalf("the script is OP_%d, push %d", script[0], script[1]) } if hex.EncodeToString(script[2:]) != hex.EncodeToString(address.Program[:]) { t.Fatal("the script's program is not the address's") } } // TestAddressIndexRange: an index at or above the hardened offset is refused, because it // would be a different derivation level rather than an address. func TestAddressIndexRange(t *testing.T) { profile, err := ProfileFor("btc", "btc") if err != nil { t.Fatal(err) } w, err := New(seedOf(t, bip39Phrase, ""), profile) if err != nil { t.Fatal(err) } if _, err := w.Address(false, 0x80000000); err == nil { t.Fatal("a hardened index was accepted as an address index") } if _, err := w.Addresses(false, 0, 0); err == nil { t.Fatal("an empty run of addresses was accepted") } run, err := w.Addresses(false, 5, 3) if err != nil { t.Fatal(err) } if len(run) != 3 { t.Fatalf("the run holds %d addresses", len(run)) } for i, address := range run { if address.Index != uint32(5+i) { t.Fatalf("address %d has index %d", i, address.Index) } } } // TestProfileNames: the repository speaks "blake" for the forked chain and "xbt" only as a // display name, so both resolve to one profile. func TestProfileNames(t *testing.T) { for _, name := range []string{"blake", "XBT"} { profile, err := ProfileFor(name, "regtest") if err != nil { t.Fatalf("%s: %v", name, err) } if profile.Name != "blake" || profile.Account != AccountBlake { t.Fatalf("%s gave %+v", name, profile) } } if _, err := ProfileFor("litecoin", "regtest"); err == nil { t.Fatal("an unknown chain resolved to a profile") } } // TestSeedIsNotInTheAddress: nothing a user shows another person carries the seed. func TestSeedIsNotInTheAddress(t *testing.T) { seed := seedOf(t, bip39Phrase, "") profile, err := ProfileFor("btc", "btc") if err != nil { t.Fatal(err) } w, err := New(seed, profile) if err != nil { t.Fatal(err) } address, err := w.Address(false, 0) if err != nil { t.Fatal(err) } encoded := hex.EncodeToString(seed[:]) if strings.Contains(address.Text, encoded[:16]) { t.Fatal("the address contains the seed") } if strings.Contains(hex.EncodeToString(address.Pubkey), encoded[:16]) { t.Fatal("the public key contains the seed") } }