Skip to content

Commit 9cd3015

Browse files
authored
Derivation Path refactor (#571)
* allow unlimited derivation path size * update docs and tests * adjust TestPaddedPublicKey to the new derivation path rules * fix derivation path algorithm; revert addresses default value * gen doc
1 parent d578aa1 commit 9cd3015

File tree

4 files changed

+215
-14
lines changed

4 files changed

+215
-14
lines changed

Diff for: cmd/wallet/usage.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
This command is meant to simplify the operations of creating
22
wallets. This command can take a seed phrase and spit out child
3-
accounts or generate new accmounts along with a seed phrase. It can
3+
accounts or generate new accounts along with a seed phrase. It can
44
generate portable wallets to be used across ETH, BTC, PoS, Substrate,
55
etc.
66

Diff for: doc/polycli_wallet.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ polycli wallet [create|inspect] [flags]
2121

2222
This command is meant to simplify the operations of creating
2323
wallets. This command can take a seed phrase and spit out child
24-
accounts or generate new accmounts along with a seed phrase. It can
24+
accounts or generate new accounts along with a seed phrase. It can
2525
generate portable wallets to be used across ETH, BTC, PoS, Substrate,
2626
etc.
2727

Diff for: hdwallet/hdwallet.go

+55-12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ethereum/go-ethereum/common"
1515
"github.com/ethereum/go-ethereum/crypto/secp256k1"
1616
"github.com/oasisprotocol/curve25519-voi/primitives/sr25519"
17+
"github.com/rs/zerolog/log"
1718
"github.com/tyler-smith/go-bip32"
1819
"github.com/tyler-smith/go-bip39"
1920
"github.com/tyler-smith/go-bip39/wordlists"
@@ -285,9 +286,56 @@ func (p *PolyWallet) ExportHDAddresses(count int) (*PolyWalletExport, error) {
285286
pwe.BIP32PublicKey = bip32Key.PublicKey().String()
286287
pwe.Addresses = make([]*PolyAddressExport, 0)
287288

288-
for i := 0; i < count; i = i + 1 {
289+
const derivationPathAddressIndexPosition = 5
290+
firstIndex := 0
291+
derivationPathParts := strings.Split(p.derivationPath, "/")
292+
if len(derivationPathParts) > 1 && count > 1 {
293+
missingElements := derivationPathAddressIndexPosition + 1 - len(derivationPathParts)
294+
for i := range missingElements {
295+
if i == 0 {
296+
if missingElements == 4 {
297+
derivationPathParts = append(derivationPathParts, "60'")
298+
} else if missingElements == 3 {
299+
derivationPathParts = append(derivationPathParts, "0'")
300+
} else {
301+
derivationPathParts = append(derivationPathParts, "0")
302+
}
303+
} else if i == 1 {
304+
if missingElements == 4 {
305+
derivationPathParts = append(derivationPathParts, "0'")
306+
} else {
307+
derivationPathParts = append(derivationPathParts, "0")
308+
}
309+
} else {
310+
derivationPathParts = append(derivationPathParts, "0")
311+
}
312+
}
313+
314+
// ensure elements 1, 2 and 3 are hardened(with apostrophe)
315+
for i := 1; i <= 3; i++ {
316+
if !strings.Contains(derivationPathParts[i], "'") {
317+
derivationPathParts[i] += "'"
318+
}
319+
}
320+
321+
lastDerivationPathPart := derivationPathParts[len(derivationPathParts)-1]
322+
lastDerivationPathPart = strings.ReplaceAll(lastDerivationPathPart, "'", "")
323+
idx, err := strconv.Atoi(lastDerivationPathPart)
324+
if err == nil {
325+
firstIndex = idx
326+
} else {
327+
log.Warn().Msg("failed to identify the index of the address in the derivation path, starting at 0")
328+
}
329+
}
330+
lastIndex := firstIndex + count
331+
332+
for i := firstIndex; i < lastIndex; i = i + 1 {
289333
// TODO if we want to provide support for hardened addresses it would need to be accommodated here
290-
currentPath := p.derivationPath + "/0/" + fmt.Sprintf("%d", i)
334+
currentPath := p.derivationPath
335+
if lastIndex-firstIndex > 1 {
336+
currentPath = strings.Join(derivationPathParts[:len(derivationPathParts)-1], "/") + "/" + strconv.Itoa(i)
337+
}
338+
291339
k, err := p.GetKeyForPath(currentPath)
292340
if err != nil {
293341
return nil, err
@@ -435,17 +483,12 @@ func parseDerivationPath(inputPath string) ([]uint32, error) {
435483
}
436484

437485
// purpose = 1, coin_type = 2, account = 3, change = 4, address_index = 5
438-
if idx >= 1 && idx <= 5 {
439-
val, err := parsePathElement(piece)
440-
if err != nil {
441-
return nil, err
442-
}
443-
path = append(path, val)
444-
}
445-
446-
if idx > 5 {
447-
return nil, fmt.Errorf("length of derivation path exceeded 5")
486+
// custom > 5
487+
val, err := parsePathElement(piece)
488+
if err != nil {
489+
return nil, err
448490
}
491+
path = append(path, val)
449492
}
450493
return path, nil
451494

Diff for: hdwallet/hdwallet_test.go

+158
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package hdwallet
22

33
import (
44
"encoding/hex"
5+
"fmt"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
810
)
911

1012
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
@@ -345,3 +347,159 @@ func TestPaddedPublicKey(t *testing.T) {
345347
t.Errorf("Unexpected address. Expected 0x2CDfa87C022744CceABC525FaA8e85Df6984A60d and Got %s", key.Addresses[1].ETHAddress)
346348
}
347349
}
350+
351+
func TestDerivationPath(t *testing.T) {
352+
type testCase struct {
353+
derivationPathInput string
354+
nAddresses int
355+
expectedSetPathError *error
356+
expectedAddresses map[string]string
357+
}
358+
359+
const mnemonic = "test test test test test test test test test test test junk"
360+
const password = ""
361+
doesntMakeSenseErrFunc := func(path string) *error {
362+
err := fmt.Errorf("the path %s doesn't seem to make sense", path)
363+
return &err
364+
}
365+
366+
testCases := []testCase{
367+
// no path derivation
368+
{"", 1, nil, map[string]string{
369+
"m/44'/60'/0'": "0x340d8879778d3D3Fec643D1736ebFd2bC5824662",
370+
}},
371+
{"", 3, nil, map[string]string{
372+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
373+
"m/44'/60'/0'/0/1": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
374+
"m/44'/60'/0'/0/2": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
375+
}},
376+
377+
// path derivation input with 1 part
378+
{"m", 1, doesntMakeSenseErrFunc("m"), nil},
379+
{"m", 3, doesntMakeSenseErrFunc("m"), nil},
380+
381+
// path derivation input with 2 parts
382+
{"m/44'", 1, nil, map[string]string{
383+
"m/44'": "0xBe0B49bD63bea56C4c18733ad9C8A41B7161318F",
384+
}},
385+
{"m/44'", 3, nil, map[string]string{
386+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
387+
"m/44'/60'/0'/0/1": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
388+
"m/44'/60'/0'/0/2": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
389+
}},
390+
391+
// path derivation input with 3 parts
392+
{"m/44'/60'", 1, nil, map[string]string{
393+
"m/44'/60'": "0x27439E87140CF69e87c89bB4C9776eAaD35BeFb3",
394+
}},
395+
{"m/44'/60'", 3, nil, map[string]string{
396+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
397+
"m/44'/60'/0'/0/1": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
398+
"m/44'/60'/0'/0/2": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
399+
}},
400+
401+
// path derivation input with 4 parts
402+
{"m/44'/60'/0'", 1, nil, map[string]string{
403+
"m/44'/60'/0'": "0x340d8879778d3D3Fec643D1736ebFd2bC5824662",
404+
}},
405+
{"m/44'/60'/0", 3, nil, map[string]string{
406+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
407+
"m/44'/60'/0'/0/1": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
408+
"m/44'/60'/0'/0/2": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
409+
}},
410+
411+
{"m/44'/60'/1", 1, nil, map[string]string{
412+
"m/44'/60'/1": "0x5600C4Cda24214FAFB227703437a3C98751C3f4F",
413+
}},
414+
{"m/44'/60'/1", 3, nil, map[string]string{
415+
"m/44'/60'/1'/0/0": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650",
416+
"m/44'/60'/1'/0/1": "0x40FBBE484b8Ee6139Af08446950B088e10b2306A",
417+
"m/44'/60'/1'/0/2": "0x2b382887D362cCae885a421C978c7e998D3c95a6",
418+
}},
419+
420+
// path derivation input with 5 parts
421+
{"m/44'/60'/0'/0", 1, nil, map[string]string{
422+
"m/44'/60'/0'/0": "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C",
423+
}},
424+
{"m/44'/60'/0'/0", 3, nil, map[string]string{
425+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
426+
"m/44'/60'/0'/0/1": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
427+
"m/44'/60'/0'/0/2": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
428+
}},
429+
430+
{"m/44'/60'/1'/2", 1, nil, map[string]string{
431+
"m/44'/60'/1'/2": "0xDd74C01e87759Ca5787C0A166103Df20a9493836",
432+
}},
433+
{"m/44'/60'/1'/2", 3, nil, map[string]string{
434+
"m/44'/60'/1'/2/0": "0x481Ea61d7635E00e32fd5BbA05E8eFe3855b0146",
435+
"m/44'/60'/1'/2/1": "0x14954c8606365f18013BCE3Af14ff4431766B3Aa",
436+
"m/44'/60'/1'/2/2": "0xC4094cD7436447541Fe5Dfe72023BBFE86799571",
437+
}},
438+
439+
// path derivation input with 6 parts
440+
{"m/44'/60'/0'/0/0", 1, nil, map[string]string{
441+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
442+
}},
443+
{"m/44'/60'/0'/0/0", 3, nil, map[string]string{
444+
"m/44'/60'/0'/0/0": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
445+
"m/44'/60'/0'/0/1": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
446+
"m/44'/60'/0'/0/2": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
447+
}},
448+
449+
{"m/44'/60'/1'/2/3", 1, nil, map[string]string{
450+
"m/44'/60'/1'/2/3": "0xC0698b4Bf4Bf25219BFF2ef077e9979DE5263b60",
451+
}},
452+
{"m/44'/60'/1'/2/3", 3, nil, map[string]string{
453+
"m/44'/60'/1'/2/3": "0xC0698b4Bf4Bf25219BFF2ef077e9979DE5263b60",
454+
"m/44'/60'/1'/2/4": "0x6B41E428F4C5a582666533B02af618291d4de347",
455+
"m/44'/60'/1'/2/5": "0x68331EB6CE792DF5c6cE927366Cb6dE41CFff51b",
456+
}},
457+
458+
// custom derivation
459+
{"m/44'/60'/1'/2/3/4/5/6/7/8/9/0", 1, nil, map[string]string{
460+
"m/44'/60'/1'/2/3/4/5/6/7/8/9/0": "0x510F2Da3BAc8Bfbf5D1b07852f48FbA3d89aFf8a",
461+
}},
462+
{"m/44'/60'/1'/2/3/4/5/6/7/8/9/0", 3, nil, map[string]string{
463+
"m/44'/60'/1'/2/3/4/5/6/7/8/9/0": "0x510F2Da3BAc8Bfbf5D1b07852f48FbA3d89aFf8a",
464+
"m/44'/60'/1'/2/3/4/5/6/7/8/9/1": "0xB3ce368159A8a60d0a71CA7161aBCa9b40fa68f4",
465+
"m/44'/60'/1'/2/3/4/5/6/7/8/9/2": "0x70DB395c0e92F3f32B6698174bBE355451Bde07A",
466+
}},
467+
468+
// op
469+
{"m/44'/60'/2'/470/10", 1, nil, map[string]string{
470+
"m/44'/60'/2'/470/10": "0x86487B98fB4BeC557dEa441C06A3c4a7feCe152F",
471+
}},
472+
{"m/44'/60'/2'/470/10", 3, nil, map[string]string{
473+
"m/44'/60'/2'/470/10": "0x86487B98fB4BeC557dEa441C06A3c4a7feCe152F",
474+
"m/44'/60'/2'/470/11": "0x2E29b5BD52b1D1D387c8dB9721Db93E8C210654E",
475+
"m/44'/60'/2'/470/12": "0x824FBFCb5F4B5dC2D01533f03C0c815a2F8Bcb03",
476+
}},
477+
}
478+
479+
for _, tc := range testCases {
480+
tcName := fmt.Sprintf("Input: \"%s\" nAddresses: %d", tc.derivationPathInput, tc.nAddresses)
481+
t.Run(tcName, func(t *testing.T) {
482+
pw, err := NewPolyWallet(mnemonic, password)
483+
require.NoError(t, err)
484+
485+
if len(tc.derivationPathInput) > 0 {
486+
err = pw.SetPath(tc.derivationPathInput)
487+
if tc.expectedSetPathError != nil {
488+
require.Error(t, err)
489+
assert.Equal(t, *tc.expectedSetPathError, err)
490+
return
491+
}
492+
require.NoError(t, err)
493+
}
494+
495+
hdAddresses, err := pw.ExportHDAddresses(tc.nAddresses)
496+
require.NoError(t, err)
497+
assert.Len(t, hdAddresses.Addresses, tc.nAddresses)
498+
499+
for _, addr := range hdAddresses.Addresses {
500+
assert.Contains(t, tc.expectedAddresses, addr.Path)
501+
assert.Equal(t, tc.expectedAddresses[addr.Path], addr.ETHAddress)
502+
}
503+
})
504+
}
505+
}

0 commit comments

Comments
 (0)