github.com/cosmos/cosmos-sdk@v0.50.1/crypto/hd/hdpath_test.go (about)

     1  package hd_test
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/cosmos/go-bip39"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/cosmos/cosmos-sdk/crypto/hd"
    12  	"github.com/cosmos/cosmos-sdk/types"
    13  )
    14  
    15  var defaultBIP39Passphrase = ""
    16  
    17  // return bip39 seed with empty passphrase
    18  func mnemonicToSeed(mnemonic string) []byte {
    19  	return bip39.NewSeed(mnemonic, defaultBIP39Passphrase)
    20  }
    21  
    22  func TestStringifyFundraiserPathParams(t *testing.T) {
    23  	path := hd.NewFundraiserParams(4, types.CoinType, 22)
    24  	require.Equal(t, "m/44'/118'/4'/0/22", path.String())
    25  
    26  	path = hd.NewFundraiserParams(4, types.CoinType, 57)
    27  	require.Equal(t, "m/44'/118'/4'/0/57", path.String())
    28  
    29  	path = hd.NewFundraiserParams(4, 12345, 57)
    30  	require.Equal(t, "m/44'/12345'/4'/0/57", path.String())
    31  }
    32  
    33  func TestPathToArray(t *testing.T) {
    34  	path := hd.NewParams(44, 118, 1, false, 4)
    35  	require.Equal(t, "[44 118 1 0 4]", fmt.Sprintf("%v", path.DerivationPath()))
    36  
    37  	path = hd.NewParams(44, 118, 2, true, 15)
    38  	require.Equal(t, "[44 118 2 1 15]", fmt.Sprintf("%v", path.DerivationPath()))
    39  }
    40  
    41  func TestParamsFromPath(t *testing.T) {
    42  	goodCases := []struct {
    43  		params *hd.BIP44Params
    44  		path   string
    45  	}{
    46  		{&hd.BIP44Params{44, 0, 0, false, 0}, "m/44'/0'/0'/0/0"},
    47  		{&hd.BIP44Params{44, 1, 0, false, 0}, "m/44'/1'/0'/0/0"},
    48  		{&hd.BIP44Params{44, 0, 1, false, 0}, "m/44'/0'/1'/0/0"},
    49  		{&hd.BIP44Params{44, 0, 0, true, 0}, "m/44'/0'/0'/1/0"},
    50  		{&hd.BIP44Params{44, 0, 0, false, 1}, "m/44'/0'/0'/0/1"},
    51  		{&hd.BIP44Params{44, 1, 1, true, 1}, "m/44'/1'/1'/1/1"},
    52  		{&hd.BIP44Params{44, 118, 52, true, 41}, "m/44'/118'/52'/1/41"},
    53  	}
    54  
    55  	for i, c := range goodCases {
    56  		params, err := hd.NewParamsFromPath(c.path)
    57  		errStr := fmt.Sprintf("%d %v", i, c)
    58  		require.NoError(t, err, errStr)
    59  		require.EqualValues(t, c.params, params, errStr)
    60  		require.Equal(t, c.path, c.params.String())
    61  	}
    62  
    63  	badCases := []struct {
    64  		path string
    65  	}{
    66  		{"m/43'/0'/0'/0/0"},   // doesn't start with 44
    67  		{"m/44'/1'/0'/0/0/5"}, // too many fields
    68  		{"m/44'/0'/1'/0"},     // too few fields
    69  		{"m/44'/0'/0'/2/0"},   // change field can only be 0/1
    70  		{"m/44/0'/0'/0/0"},    // first field needs '
    71  		{"m/44'/0/0'/0/0"},    // second field needs '
    72  		{"m/44'/0'/0/0/0"},    // third field needs '
    73  		{"m/44'/0'/0'/0'/0"},  // fourth field must not have '
    74  		{"m/44'/0'/0'/0/0'"},  // fifth field must not have '
    75  		{"m/44'/-1'/0'/0/0"},  // no negatives
    76  		{"m/44'/0'/0'/-1/0"},  // no negatives
    77  		{"m/a'/0'/0'/-1/0"},   // invalid values
    78  		{"m/0/X/0'/-1/0"},     // invalid values
    79  		{"m/44'/0'/X/-1/0"},   // invalid values
    80  		{"m/44'/0'/0'/%/0"},   // invalid values
    81  		{"m/44'/0'/0'/0/%"},   // invalid values
    82  		{"m44'0'0'00"},        // no separators
    83  		{" /44'/0'/0'/0/0"},   // blank first component
    84  	}
    85  
    86  	for i, c := range badCases {
    87  		params, err := hd.NewParamsFromPath(c.path)
    88  		errStr := fmt.Sprintf("%d %v", i, c)
    89  		require.Nil(t, params, errStr)
    90  		require.Error(t, err, errStr)
    91  	}
    92  }
    93  
    94  func TestCreateHDPath(t *testing.T) {
    95  	type args struct {
    96  		coinType uint32
    97  		account  uint32
    98  		index    uint32
    99  	}
   100  	tests := []struct {
   101  		name string
   102  		args args
   103  		want hd.BIP44Params
   104  	}{
   105  		{"m/44'/0'/0'/0/0", args{0, 0, 0}, hd.BIP44Params{Purpose: 44}},
   106  		{"m/44'/114'/0'/0/0", args{114, 0, 0}, hd.BIP44Params{Purpose: 44, CoinType: 114, Account: 0, AddressIndex: 0}},
   107  		{"m/44'/114'/1'/1/0", args{114, 1, 1}, hd.BIP44Params{Purpose: 44, CoinType: 114, Account: 1, AddressIndex: 1}},
   108  	}
   109  	for _, tt := range tests {
   110  		tt := tt
   111  		t.Run(tt.name, func(t *testing.T) {
   112  			tt := tt
   113  			require.Equal(t, tt.want, *hd.CreateHDPath(tt.args.coinType, tt.args.account, tt.args.index))
   114  		})
   115  	}
   116  }
   117  
   118  // Tests to ensure that any index value is in the range [0, max(int32)] as per
   119  // the extended keys specification. If the index belongs to that of a hardened key,
   120  // its 0x80000000 bit will be set, so we can still accept values in [0, max(int32)] and then
   121  // increase its value as deriveKeyPath already augments.
   122  // See issue https://github.com/cosmos/cosmos-sdk/issues/7627.
   123  func TestDeriveHDPathRange(t *testing.T) {
   124  	seed := mnemonicToSeed("I am become Death, the destroyer of worlds!")
   125  
   126  	tests := []struct {
   127  		path    string
   128  		wantErr string
   129  	}{
   130  		{
   131  			path:    "m/1'/2147483648/0'/0/0",
   132  			wantErr: "out of range",
   133  		},
   134  		{
   135  			path:    "m/2147483648'/1/0/0",
   136  			wantErr: "out of range",
   137  		},
   138  		{
   139  			path:    "m/2147483648'/2147483648/0'/0/0",
   140  			wantErr: "out of range",
   141  		},
   142  		{
   143  			path:    "m/1'/-5/0'/0/0",
   144  			wantErr: "invalid syntax",
   145  		},
   146  		{
   147  			path:    "m/-2147483646'/1/0/0",
   148  			wantErr: "invalid syntax",
   149  		},
   150  		{
   151  			path:    "m/-2147483648'/-2147483648/0'/0/0",
   152  			wantErr: "invalid syntax",
   153  		},
   154  		{
   155  			path:    "m44'118'0'00",
   156  			wantErr: "path 'm44'118'0'00' doesn't contain '/' separators",
   157  		},
   158  		{
   159  			path:    "",
   160  			wantErr: "path '' doesn't contain '/' separators",
   161  		},
   162  		{
   163  			// Should pass.
   164  			path: "m/1'/2147483647'/1/0'/0/0",
   165  		},
   166  		{
   167  			// Should pass.
   168  			path: "1'/2147483647'/1/0'/0/0",
   169  		},
   170  	}
   171  
   172  	for _, tt := range tests {
   173  		tt := tt
   174  		t.Run(tt.path, func(t *testing.T) {
   175  			master, ch := hd.ComputeMastersFromSeed(seed)
   176  			_, err := hd.DerivePrivateKeyForPath(master, ch, tt.path)
   177  
   178  			if tt.wantErr == "" {
   179  				require.NoError(t, err, "unexpected error")
   180  			} else {
   181  				require.Error(t, err, "expected a report of an int overflow")
   182  				require.Contains(t, err.Error(), tt.wantErr)
   183  			}
   184  		})
   185  	}
   186  }
   187  
   188  func ExampleStringifyPathParams() { //nolint:govet // ignore naming convention
   189  	path := hd.NewParams(44, 0, 0, false, 0)
   190  	fmt.Println(path.String())
   191  	path = hd.NewParams(44, 33, 7, true, 9)
   192  	fmt.Println(path.String())
   193  	// Output:
   194  	// m/44'/0'/0'/0/0
   195  	// m/44'/33'/7'/1/9
   196  }
   197  
   198  func ExampleSomeBIP32TestVecs() { //nolint:govet // ignore naming convention
   199  	seed := mnemonicToSeed("barrel original fuel morning among eternal " +
   200  		"filter ball stove pluck matrix mechanic")
   201  	master, ch := hd.ComputeMastersFromSeed(seed)
   202  	fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)")
   203  	fmt.Println()
   204  	// cosmos
   205  	priv, err := hd.DerivePrivateKeyForPath(master, ch, types.FullFundraiserPath)
   206  	if err != nil {
   207  		fmt.Println("INVALID")
   208  	} else {
   209  		fmt.Println(hex.EncodeToString(priv))
   210  	}
   211  	// bitcoin
   212  	priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0")
   213  	if err != nil {
   214  		fmt.Println("INVALID")
   215  	} else {
   216  		fmt.Println(hex.EncodeToString(priv))
   217  	}
   218  	// ether
   219  	priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0")
   220  	if err != nil {
   221  		fmt.Println("INVALID")
   222  	} else {
   223  		fmt.Println(hex.EncodeToString(priv))
   224  	}
   225  	// INVALID
   226  	priv, err = hd.DerivePrivateKeyForPath(master, ch, "X/0'/0'/0/0")
   227  	if err != nil {
   228  		fmt.Println("INVALID")
   229  	} else {
   230  		fmt.Println(hex.EncodeToString(priv))
   231  	}
   232  	priv, err = hd.DerivePrivateKeyForPath(master, ch, "-44/0'/0'/0/0")
   233  	if err != nil {
   234  		fmt.Println("INVALID")
   235  	} else {
   236  		fmt.Println(hex.EncodeToString(priv))
   237  	}
   238  
   239  	fmt.Println()
   240  	fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html")
   241  	fmt.Println()
   242  
   243  	seed = mnemonicToSeed(
   244  		"advice process birth april short trust crater change bacon monkey medal garment " +
   245  			"gorilla ranch hour rival razor call lunar mention taste vacant woman sister")
   246  	master, ch = hd.ComputeMastersFromSeed(seed)
   247  	priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4")
   248  	fmt.Println(hex.EncodeToString(priv))
   249  
   250  	seed = mnemonicToSeed("idea naive region square margin day captain habit " +
   251  		"gun second farm pact pulse someone armed")
   252  	master, ch = hd.ComputeMastersFromSeed(seed)
   253  	priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420")
   254  	fmt.Println(hex.EncodeToString(priv))
   255  
   256  	fmt.Println()
   257  	fmt.Println("BIP 32 example")
   258  	fmt.Println()
   259  
   260  	// bip32 path: m/0/7
   261  	seed = mnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history")
   262  	master, ch = hd.ComputeMastersFromSeed(seed)
   263  	priv, _ = hd.DerivePrivateKeyForPath(master, ch, "0/7")
   264  	fmt.Println(hex.EncodeToString(priv))
   265  
   266  	// Output: keys from fundraiser test-vector (cosmos, bitcoin, ether)
   267  	//
   268  	// bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c
   269  	// e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d
   270  	// 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc
   271  	// INVALID
   272  	// INVALID
   273  	//
   274  	// keys generated via https://coinomi.com/recovery-phrase-tool.html
   275  	//
   276  	// a61f10c5fecf40c084c94fa54273b6f5d7989386be4a37669e6d6f7b0169c163
   277  	// 32c4599843de3ef161a629a461d12c60b009b676c35050be5f7ded3a3b23501f
   278  	//
   279  	// BIP 32 example
   280  	//
   281  	// c4c11d8c03625515905d7e89d25dfc66126fbc629ecca6db489a1a72fc4bda78
   282  }
   283  
   284  // Ensuring that we don't crash if values have trailing slashes
   285  // See issue https://github.com/cosmos/cosmos-sdk/issues/8557.
   286  func TestDerivePrivateKeyForPathDoNotCrash(t *testing.T) {
   287  	paths := []string{
   288  		"m/5/",
   289  		"m/5",
   290  		"/44",
   291  		"m//5",
   292  		"m/0/7",
   293  		"/",
   294  		" m       /0/7",          // Test case from fuzzer
   295  		"              /       ", // Test case from fuzzer
   296  		"m///7//////",
   297  	}
   298  
   299  	for _, path := range paths {
   300  		path := path
   301  		t.Run(path, func(t *testing.T) {
   302  			_, _ = hd.DerivePrivateKeyForPath([32]byte{}, [32]byte{}, path)
   303  		})
   304  	}
   305  }