package hd_test import ( "encoding/hex" "fmt" "testing" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/types" bip39 "github.com/cosmos/go-bip39" "github.com/stretchr/testify/require" ) var defaultBIP39Passphrase = "" // return bip39 seed with empty passphrase func mnemonicToSeed(mnemonic string) []byte { return bip39.NewSeed(mnemonic, defaultBIP39Passphrase) } func TestStringifyFundraiserPathParams(t *testing.T) { path := hd.NewFundraiserParams(4, types.CoinType, 22) require.Equal(t, "m/44'/118'/4'/0/22", path.String()) path = hd.NewFundraiserParams(4, types.CoinType, 57) require.Equal(t, "m/44'/118'/4'/0/57", path.String()) path = hd.NewFundraiserParams(4, 12345, 57) require.Equal(t, "m/44'/12345'/4'/0/57", path.String()) } func TestPathToArray(t *testing.T) { path := hd.NewParams(44, 118, 1, false, 4) require.Equal(t, "[44 118 1 0 4]", fmt.Sprintf("%v", path.DerivationPath())) path = hd.NewParams(44, 118, 2, true, 15) require.Equal(t, "[44 118 2 1 15]", fmt.Sprintf("%v", path.DerivationPath())) } func TestParamsFromPath(t *testing.T) { goodCases := []struct { params *hd.BIP44Params path string }{ {&hd.BIP44Params{44, 0, 0, false, 0}, "m/44'/0'/0'/0/0"}, {&hd.BIP44Params{44, 1, 0, false, 0}, "m/44'/1'/0'/0/0"}, {&hd.BIP44Params{44, 0, 1, false, 0}, "m/44'/0'/1'/0/0"}, {&hd.BIP44Params{44, 0, 0, true, 0}, "m/44'/0'/0'/1/0"}, {&hd.BIP44Params{44, 0, 0, false, 1}, "m/44'/0'/0'/0/1"}, {&hd.BIP44Params{44, 1, 1, true, 1}, "m/44'/1'/1'/1/1"}, {&hd.BIP44Params{44, 118, 52, true, 41}, "m/44'/118'/52'/1/41"}, } for i, c := range goodCases { params, err := hd.NewParamsFromPath(c.path) errStr := fmt.Sprintf("%d %v", i, c) require.NoError(t, err, errStr) require.EqualValues(t, c.params, params, errStr) require.Equal(t, c.path, c.params.String()) } badCases := []struct { path string }{ {"m/43'/0'/0'/0/0"}, // doesn't start with 44 {"m/44'/1'/0'/0/0/5"}, // too many fields {"m/44'/0'/1'/0"}, // too few fields {"m/44'/0'/0'/2/0"}, // change field can only be 0/1 {"m/44/0'/0'/0/0"}, // first field needs ' {"m/44'/0/0'/0/0"}, // second field needs ' {"m/44'/0'/0/0/0"}, // third field needs ' {"m/44'/0'/0'/0'/0"}, // fourth field must not have ' {"m/44'/0'/0'/0/0'"}, // fifth field must not have ' {"m/44'/-1'/0'/0/0"}, // no negatives {"m/44'/0'/0'/-1/0"}, // no negatives {"m/a'/0'/0'/-1/0"}, // invalid values {"m/0/X/0'/-1/0"}, // invalid values {"m/44'/0'/X/-1/0"}, // invalid values {"m/44'/0'/0'/%/0"}, // invalid values {"m/44'/0'/0'/0/%"}, // invalid values {"m44'0'0'00"}, // no separators {" /44'/0'/0'/0/0"}, // blank first component } for i, c := range badCases { params, err := hd.NewParamsFromPath(c.path) errStr := fmt.Sprintf("%d %v", i, c) require.Nil(t, params, errStr) require.Error(t, err, errStr) } } func TestCreateHDPath(t *testing.T) { type args struct { coinType uint32 account uint32 index uint32 } tests := []struct { name string args args want hd.BIP44Params }{ {"m/44'/0'/0'/0/0", args{0, 0, 0}, hd.BIP44Params{Purpose: 44}}, {"m/44'/114'/0'/0/0", args{114, 0, 0}, hd.BIP44Params{Purpose: 44, CoinType: 114, Account: 0, AddressIndex: 0}}, {"m/44'/114'/1'/1/0", args{114, 1, 1}, hd.BIP44Params{Purpose: 44, CoinType: 114, Account: 1, AddressIndex: 1}}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { tt := tt require.Equal(t, tt.want, *hd.CreateHDPath(tt.args.coinType, tt.args.account, tt.args.index)) }) } } // Tests to ensure that any index value is in the range [0, max(int32)] as per // the extended keys specification. If the index belongs to that of a hardened key, // its 0x80000000 bit will be set, so we can still accept values in [0, max(int32)] and then // increase its value as deriveKeyPath already augments. // See issue https://github.com/cosmos/cosmos-sdk/issues/7627. func TestDeriveHDPathRange(t *testing.T) { seed := mnemonicToSeed("I am become Death, the destroyer of worlds!") tests := []struct { path string wantErr string }{ { path: "m/1'/2147483648/0'/0/0", wantErr: "out of range", }, { path: "m/2147483648'/1/0/0", wantErr: "out of range", }, { path: "m/2147483648'/2147483648/0'/0/0", wantErr: "out of range", }, { path: "m/1'/-5/0'/0/0", wantErr: "invalid syntax", }, { path: "m/-2147483646'/1/0/0", wantErr: "invalid syntax", }, { path: "m/-2147483648'/-2147483648/0'/0/0", wantErr: "invalid syntax", }, { path: "m44'118'0'00", wantErr: "path 'm44'118'0'00' doesn't contain '/' separators", }, { path: "", wantErr: "path '' doesn't contain '/' separators", }, { // Should pass. path: "m/1'/2147483647'/1/0'/0/0", }, { // Should pass. path: "1'/2147483647'/1/0'/0/0", }, } for _, tt := range tests { tt := tt t.Run(tt.path, func(t *testing.T) { master, ch := hd.ComputeMastersFromSeed(seed) _, err := hd.DerivePrivateKeyForPath(master, ch, tt.path) if tt.wantErr == "" { require.NoError(t, err, "unexpected error") } else { require.Error(t, err, "expected a report of an int overflow") require.Contains(t, err.Error(), tt.wantErr) } }) } } func ExampleStringifyPathParams() { path := hd.NewParams(44, 0, 0, false, 0) fmt.Println(path.String()) path = hd.NewParams(44, 33, 7, true, 9) fmt.Println(path.String()) // Output: // m/44'/0'/0'/0/0 // m/44'/33'/7'/1/9 } func ExampleSomeBIP32TestVecs() { seed := mnemonicToSeed("barrel original fuel morning among eternal " + "filter ball stove pluck matrix mechanic") master, ch := hd.ComputeMastersFromSeed(seed) fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)") fmt.Println() // cosmos priv, err := hd.DerivePrivateKeyForPath(master, ch, types.FullFundraiserPath) if err != nil { fmt.Println("INVALID") } else { fmt.Println(hex.EncodeToString(priv[:])) } // bitcoin priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0") if err != nil { fmt.Println("INVALID") } else { fmt.Println(hex.EncodeToString(priv[:])) } // ether priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0") if err != nil { fmt.Println("INVALID") } else { fmt.Println(hex.EncodeToString(priv[:])) } // INVALID priv, err = hd.DerivePrivateKeyForPath(master, ch, "X/0'/0'/0/0") if err != nil { fmt.Println("INVALID") } else { fmt.Println(hex.EncodeToString(priv[:])) } priv, err = hd.DerivePrivateKeyForPath(master, ch, "-44/0'/0'/0/0") if err != nil { fmt.Println("INVALID") } else { fmt.Println(hex.EncodeToString(priv[:])) } fmt.Println() fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html") fmt.Println() seed = mnemonicToSeed( "advice process birth april short trust crater change bacon monkey medal garment " + "gorilla ranch hour rival razor call lunar mention taste vacant woman sister") master, ch = hd.ComputeMastersFromSeed(seed) priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4") fmt.Println(hex.EncodeToString(priv[:])) seed = mnemonicToSeed("idea naive region square margin day captain habit " + "gun second farm pact pulse someone armed") master, ch = hd.ComputeMastersFromSeed(seed) priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420") fmt.Println(hex.EncodeToString(priv[:])) fmt.Println() fmt.Println("BIP 32 example") fmt.Println() // bip32 path: m/0/7 seed = mnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history") master, ch = hd.ComputeMastersFromSeed(seed) priv, _ = hd.DerivePrivateKeyForPath(master, ch, "0/7") fmt.Println(hex.EncodeToString(priv[:])) // Output: keys from fundraiser test-vector (cosmos, bitcoin, ether) // // bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c // e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d // 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc // INVALID // INVALID // // keys generated via https://coinomi.com/recovery-phrase-tool.html // // a61f10c5fecf40c084c94fa54273b6f5d7989386be4a37669e6d6f7b0169c163 // 32c4599843de3ef161a629a461d12c60b009b676c35050be5f7ded3a3b23501f // // BIP 32 example // // c4c11d8c03625515905d7e89d25dfc66126fbc629ecca6db489a1a72fc4bda78 } // Ensuring that we don't crash if values have trailing slashes // See issue https://github.com/cosmos/cosmos-sdk/issues/8557. func TestDerivePrivateKeyForPathDoNotCrash(t *testing.T) { paths := []string{ "m/5/", "m/5", "/44", "m//5", "m/0/7", "/", " m  /0/7", // Test case from fuzzer "  /  ", // Test case from fuzzer "m///7//////", } for _, path := range paths { path := path t.Run(path, func(t *testing.T) { hd.DerivePrivateKeyForPath([32]byte{}, [32]byte{}, path) }) } }