wallet_test.go raw

   1  package wallet
   2  
   3  import (
   4  	"encoding/hex"
   5  	"strings"
   6  	"testing"
   7  
   8  	"git.smesh.lol/tinywallet/pkg/bech32"
   9  	"git.smesh.lol/tinywallet/pkg/mnemonic"
  10  )
  11  
  12  // bip39Phrase is the reference phrase the BIP84 vectors use: twelve words of zero entropy.
  13  const bip39Phrase = "abandon abandon abandon abandon abandon abandon " +
  14  	"abandon abandon abandon abandon abandon about"
  15  
  16  // seedOf derives the reference seed for a phrase and passphrase.
  17  func seedOf(t *testing.T, phrase, passphrase string) [64]byte {
  18  	t.Helper()
  19  
  20  	seed, err := mnemonic.Seed(phrase, passphrase)
  21  
  22  	if err != nil {
  23  		t.Fatal(err)
  24  	}
  25  
  26  	return seed
  27  }
  28  
  29  // TestBIP84Vectors derives the three addresses the BIP84 specification publishes.
  30  //
  31  // It is the whole stack in one assertion: the BIP39 seed, BIP32 private derivation, the
  32  // secp256k1 public key, hash160 and the bech32 encoder. If any of them is wrong the address
  33  // is not this one, and the specification is the authority rather than this package's own
  34  // output.
  35  func TestBIP84Vectors(t *testing.T) {
  36  	profile, err := ProfileFor("btc", "btc")
  37  
  38  	if err != nil {
  39  		t.Fatal(err)
  40  	}
  41  
  42  	w, err := New(seedOf(t, bip39Phrase, ""), profile)
  43  
  44  	if err != nil {
  45  		t.Fatal(err)
  46  	}
  47  
  48  	for _, test := range []struct {
  49  		change  bool
  50  		index   uint32
  51  		pubkey  string
  52  		address string
  53  	}{
  54  		{
  55  			change: false, index: 0,
  56  			pubkey:  "0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c",
  57  			address: "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
  58  		},
  59  		{
  60  			change: false, index: 1,
  61  			pubkey:  "03e775fd51f0dfb8cd865d9ff1cca2a158cf651fe997fdc9fee9c1d3b5e995ea77",
  62  			address: "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g",
  63  		},
  64  		{
  65  			change: true, index: 0,
  66  			pubkey:  "03025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a6",
  67  			address: "bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el",
  68  		},
  69  	} {
  70  		address, err := w.Address(test.change, test.index)
  71  
  72  		if err != nil {
  73  			t.Fatalf("%v/%d: %v", test.change, test.index, err)
  74  		}
  75  
  76  		if hex.EncodeToString(address.Pubkey) != test.pubkey {
  77  			t.Fatalf("%v/%d pubkey is %s, want %s", test.change, test.index,
  78  				hex.EncodeToString(address.Pubkey), test.pubkey)
  79  		}
  80  
  81  		if address.Text != test.address {
  82  			t.Fatalf("%v/%d address is %s, want %s", test.change, test.index,
  83  				address.Text, test.address)
  84  		}
  85  
  86  		if address.Path.String() != pathString(test.change, test.index) {
  87  			t.Fatalf("%v/%d path is %s", test.change, test.index, address.Path)
  88  		}
  89  	}
  90  }
  91  
  92  // pathString is the path the BIP84 vectors name.
  93  func pathString(change bool, index uint32) string {
  94  	branch := "0"
  95  
  96  	if change {
  97  		branch = "1"
  98  	}
  99  
 100  	return "m/84'/0'/0'/" + branch + "/" + itoa(index)
 101  }
 102  
 103  // itoa renders a small index.
 104  func itoa(value uint32) string {
 105  	if value == 0 {
 106  		return "0"
 107  	}
 108  
 109  	var digits []byte
 110  
 111  	for value > 0 {
 112  		digits = append([]byte{byte('0' + value%10)}, digits...)
 113  		value /= 10
 114  	}
 115  
 116  	return string(digits)
 117  }
 118  
 119  // TestGeneratorAddress: the P2WPKH address of the secp256k1 generator point is the
 120  // canonical BIP173 example, so the address encoder is checked against something other than
 121  // the derivation that feeds it.
 122  func TestGeneratorAddress(t *testing.T) {
 123  	pubkey, err := hex.DecodeString(
 124  		"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
 125  
 126  	if err != nil {
 127  		t.Fatal(err)
 128  	}
 129  
 130  	program := hash160(pubkey)
 131  
 132  	address, err := bech32.EncodeSegwit("bc", 0, program[:])
 133  
 134  	if err != nil {
 135  		t.Fatal(err)
 136  	}
 137  
 138  	if address != "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" {
 139  		t.Fatalf("the generator address is %s", address)
 140  	}
 141  }
 142  
 143  // TestTheTwoChainsDeriveDifferentKeys: one seed, two accounts, no shared key.
 144  //
 145  // This is the property the forked chain depends on: it has no registered coin type, so it
 146  // takes a second account under the same one, and a mistake here would put the same key on
 147  // two chains.
 148  func TestTheTwoChainsDeriveDifferentKeys(t *testing.T) {
 149  	seed := seedOf(t, bip39Phrase, "")
 150  
 151  	btcProfile, err := ProfileFor("btc", "regtest")
 152  
 153  	if err != nil {
 154  		t.Fatal(err)
 155  	}
 156  
 157  	blakeProfile, err := ProfileFor("blake", "regtest")
 158  
 159  	if err != nil {
 160  		t.Fatal(err)
 161  	}
 162  
 163  	btc, err := New(seed, btcProfile)
 164  
 165  	if err != nil {
 166  		t.Fatal(err)
 167  	}
 168  
 169  	blake, err := New(seed, blakeProfile)
 170  
 171  	if err != nil {
 172  		t.Fatal(err)
 173  	}
 174  
 175  	btcAddress, err := btc.Address(false, 0)
 176  
 177  	if err != nil {
 178  		t.Fatal(err)
 179  	}
 180  
 181  	blakeAddress, err := blake.Address(false, 0)
 182  
 183  	if err != nil {
 184  		t.Fatal(err)
 185  	}
 186  
 187  	if hex.EncodeToString(btcAddress.Pubkey) == hex.EncodeToString(blakeAddress.Pubkey) {
 188  		t.Fatal("the two chains derived the same public key")
 189  	}
 190  
 191  	if btcAddress.Program == blakeAddress.Program {
 192  		t.Fatal("the two chains derived the same witness program")
 193  	}
 194  
 195  	if btcAddress.Text == blakeAddress.Text {
 196  		t.Fatal("the two chains derived the same address")
 197  	}
 198  
 199  	// Both chains are on the same network, so they share the coin type and differ only in
 200  	// the account: that is what one seed, two wallets means.
 201  	if btcAddress.Path.String() != "m/84'/1'/0'/0/0" {
 202  		t.Fatalf("the btc path is %s", btcAddress.Path)
 203  	}
 204  
 205  	if blakeAddress.Path.String() != "m/84'/1'/1'/0/0" {
 206  		t.Fatalf("the blake path is %s", blakeAddress.Path)
 207  	}
 208  
 209  	// The two profiles are still the same network family: only the account differs.
 210  	if btcProfile.CoinType != blakeProfile.CoinType {
 211  		t.Fatalf("the coin types differ: %d and %d", btcProfile.CoinType,
 212  			blakeProfile.CoinType)
 213  	}
 214  
 215  	// Both regtest profiles carry the regtest prefix, which is the point: the fork is the
 216  	// same network as Bitcoin, so the account is what separates the two wallets.
 217  	if btcProfile.HRP != "bcrt" || blakeProfile.HRP != "bcrt" {
 218  		t.Fatalf("the regtest prefixes are %q and %q", btcProfile.HRP, blakeProfile.HRP)
 219  	}
 220  }
 221  
 222  // TestAddressIsChainScoped: an address carries the prefix of its chain, and decoding one
 223  // under another chain's prefix is a different string.
 224  func TestAddressIsChainScoped(t *testing.T) {
 225  	profile, err := ProfileFor("btc", "btc")
 226  
 227  	if err != nil {
 228  		t.Fatal(err)
 229  	}
 230  
 231  	// The rig is regtest, so the prefix the wallet uses there is bcrt.
 232  	profile.HRP = "bcrt"
 233  
 234  	w, err := New(seedOf(t, bip39Phrase, ""), profile)
 235  
 236  	if err != nil {
 237  		t.Fatal(err)
 238  	}
 239  
 240  	address, err := w.Address(false, 0)
 241  
 242  	if err != nil {
 243  		t.Fatal(err)
 244  	}
 245  
 246  	if !strings.HasPrefix(address.Text, "bcrt1") {
 247  		t.Fatalf("the regtest address is %s", address.Text)
 248  	}
 249  
 250  	hrp, version, program, err := bech32.DecodeSegwit(address.Text)
 251  
 252  	if err != nil {
 253  		t.Fatal(err)
 254  	}
 255  
 256  	if hrp != "bcrt" || version != 0 || program == nil {
 257  		t.Fatalf("decoded to %q/%d/%x", hrp, version, program)
 258  	}
 259  
 260  	if address.Chain != "btc" {
 261  		t.Fatalf("the address names chain %q", address.Chain)
 262  	}
 263  
 264  	// The same program under the wrong prefix is not the same address, which is what makes
 265  	// showing a BTC address as an XBT one a real mistake rather than a cosmetic one.
 266  	mainnet, err := bech32.EncodeSegwit("bc", 0, program)
 267  
 268  	if err != nil {
 269  		t.Fatal(err)
 270  	}
 271  
 272  	if mainnet == address.Text {
 273  		t.Fatal("an address is identical under two prefixes")
 274  	}
 275  }
 276  
 277  // TestScript: the filter client matches a script, not an address.
 278  func TestScript(t *testing.T) {
 279  	profile, err := ProfileFor("btc", "btc")
 280  
 281  	if err != nil {
 282  		t.Fatal(err)
 283  	}
 284  
 285  	w, err := New(seedOf(t, bip39Phrase, ""), profile)
 286  
 287  	if err != nil {
 288  		t.Fatal(err)
 289  	}
 290  
 291  	address, err := w.Address(false, 0)
 292  
 293  	if err != nil {
 294  		t.Fatal(err)
 295  	}
 296  
 297  	script := address.Script()
 298  
 299  	if len(script) != 22 {
 300  		t.Fatalf("the script is %d bytes", len(script))
 301  	}
 302  
 303  	if script[0] != 0x00 || script[1] != 0x14 {
 304  		t.Fatalf("the script is OP_%d, push %d", script[0], script[1])
 305  	}
 306  
 307  	if hex.EncodeToString(script[2:]) != hex.EncodeToString(address.Program[:]) {
 308  		t.Fatal("the script's program is not the address's")
 309  	}
 310  }
 311  
 312  // TestAddressIndexRange: an index at or above the hardened offset is refused, because it
 313  // would be a different derivation level rather than an address.
 314  func TestAddressIndexRange(t *testing.T) {
 315  	profile, err := ProfileFor("btc", "btc")
 316  
 317  	if err != nil {
 318  		t.Fatal(err)
 319  	}
 320  
 321  	w, err := New(seedOf(t, bip39Phrase, ""), profile)
 322  
 323  	if err != nil {
 324  		t.Fatal(err)
 325  	}
 326  
 327  	if _, err := w.Address(false, 0x80000000); err == nil {
 328  		t.Fatal("a hardened index was accepted as an address index")
 329  	}
 330  
 331  	if _, err := w.Addresses(false, 0, 0); err == nil {
 332  		t.Fatal("an empty run of addresses was accepted")
 333  	}
 334  
 335  	run, err := w.Addresses(false, 5, 3)
 336  
 337  	if err != nil {
 338  		t.Fatal(err)
 339  	}
 340  
 341  	if len(run) != 3 {
 342  		t.Fatalf("the run holds %d addresses", len(run))
 343  	}
 344  
 345  	for i, address := range run {
 346  		if address.Index != uint32(5+i) {
 347  			t.Fatalf("address %d has index %d", i, address.Index)
 348  		}
 349  	}
 350  }
 351  
 352  // TestProfileNames: the repository speaks "blake" for the forked chain and "xbt" only as a
 353  // display name, so both resolve to one profile.
 354  func TestProfileNames(t *testing.T) {
 355  	for _, name := range []string{"blake", "XBT"} {
 356  		profile, err := ProfileFor(name, "regtest")
 357  
 358  		if err != nil {
 359  			t.Fatalf("%s: %v", name, err)
 360  		}
 361  
 362  		if profile.Name != "blake" || profile.Account != AccountBlake {
 363  			t.Fatalf("%s gave %+v", name, profile)
 364  		}
 365  	}
 366  
 367  	if _, err := ProfileFor("litecoin", "regtest"); err == nil {
 368  		t.Fatal("an unknown chain resolved to a profile")
 369  	}
 370  }
 371  
 372  // TestSeedIsNotInTheAddress: nothing a user shows another person carries the seed.
 373  func TestSeedIsNotInTheAddress(t *testing.T) {
 374  	seed := seedOf(t, bip39Phrase, "")
 375  
 376  	profile, err := ProfileFor("btc", "btc")
 377  
 378  	if err != nil {
 379  		t.Fatal(err)
 380  	}
 381  
 382  	w, err := New(seed, profile)
 383  
 384  	if err != nil {
 385  		t.Fatal(err)
 386  	}
 387  
 388  	address, err := w.Address(false, 0)
 389  
 390  	if err != nil {
 391  		t.Fatal(err)
 392  	}
 393  
 394  	encoded := hex.EncodeToString(seed[:])
 395  
 396  	if strings.Contains(address.Text, encoded[:16]) {
 397  		t.Fatal("the address contains the seed")
 398  	}
 399  
 400  	if strings.Contains(hex.EncodeToString(address.Pubkey), encoded[:16]) {
 401  		t.Fatal("the public key contains the seed")
 402  	}
 403  }
 404