Skip to content

Commit 023994c

Browse files
authored
Merge pull request #3 from libsv/enhancement/seeded_derivationpath
2 parents d12291b + efbec77 commit 023994c

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

bip32/derivationpaths.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package bip32
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// DerivePath given an uint64 number will generate a hardened BIP32 path 3 layers deep.
11+
//
12+
// This is achieved by the following process:
13+
// We split the seed bits into 3 sections: (b63-b32|b32-b1|b1-b0)
14+
// Each section is then added onto 2^31 and concatenated together which will give us the final path.
15+
func DerivePath(i uint64) string {
16+
path := fmt.Sprintf("%d/", i>>32|1<<31)
17+
path += fmt.Sprintf("%d/", ((i<<32)>>34)|1<<31)
18+
path += fmt.Sprintf("%d", (i&3)|1<<31)
19+
return path
20+
}
21+
22+
// DeriveNumber when given a derivation path of format 0/0/0 will
23+
// reverse the DerivePath function and return the number used to generate
24+
// the path.
25+
func DeriveNumber(path string) (uint64, error) {
26+
ss := strings.Split(path, "/")
27+
if len(ss) != 3 {
28+
return 0, errors.New("path must have 3 levels ie 0/0/0")
29+
}
30+
d1, err := strconv.ParseUint(ss[0], 10, 32)
31+
if err != nil {
32+
return 0, err
33+
}
34+
seed := (d1 - 1<<31) << 32
35+
d2, err := strconv.ParseUint(ss[1], 10, 32)
36+
if err != nil {
37+
return 0, err
38+
}
39+
seed += (d2 - (1 << 31)) << 2
40+
d3, err := strconv.ParseUint(ss[2], 10, 32)
41+
if err != nil {
42+
return 0, err
43+
}
44+
seed += d3 - (1 << 31)
45+
return seed, nil
46+
}

bip32/derivationpaths_test.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package bip32
2+
3+
import (
4+
"errors"
5+
"math"
6+
"strconv"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func Test_DerivePathAndDeriveSeed(t *testing.T) {
13+
t.Parallel()
14+
tests := map[string]struct {
15+
counter uint64
16+
startingPath string
17+
exp string
18+
}{
19+
"successful run should return no errors": {
20+
counter: 0,
21+
startingPath: "",
22+
exp: "2147483648/2147483648/2147483648",
23+
}, "172732732 counter with 0 path": {
24+
counter: 172732732,
25+
startingPath: "",
26+
exp: "2147483648/2190666831/2147483648",
27+
}, "max int should return max int with root path": {
28+
counter: math.MaxInt32 + 172732732,
29+
startingPath: "",
30+
exp: "2147483648/2727537742/2147483651",
31+
}, "max int * 2 should return 1 with root path": {
32+
counter: (math.MaxInt32 * 10000) + 172732732,
33+
startingPath: "",
34+
exp: "2147488648/2190664331/2147483648",
35+
}, "max int squared should return 0/0 path": {
36+
counter: (math.MaxInt32 * math.MaxInt32) + 172732732,
37+
startingPath: "",
38+
exp: "3221225471/2190666831/2147483649",
39+
}, "max int squared + 100 should return correct path": {
40+
counter: (math.MaxInt32*math.MaxInt32 + (math.MaxInt32 * 100)) + 172732732,
41+
startingPath: "",
42+
exp: "3221225521/2190666806/2147483649",
43+
}, "max int squared plus two int32 should return correct path": {
44+
counter: ((math.MaxInt32 * math.MaxInt32 * 1) + (math.MaxInt32 * 2)) + 172732732,
45+
startingPath: "",
46+
exp: "3221225472/2190666830/2147483651",
47+
},
48+
}
49+
for name, test := range tests {
50+
t.Run(name, func(t *testing.T) {
51+
assert.Equal(t, test.exp, DerivePath(test.counter))
52+
// assert the path can be converted correctly back to the seed.
53+
c, _ := DeriveNumber(test.exp)
54+
assert.Equal(t, test.counter, c)
55+
})
56+
}
57+
}
58+
59+
func TestDeriveSeed(t *testing.T) {
60+
t.Parallel()
61+
tests := map[string]struct {
62+
path string
63+
err error
64+
}{
65+
"missing path should error": {
66+
err: errors.New("path must have 3 levels ie 0/0/0"),
67+
},
68+
"path too long should error": {
69+
path: "0/0/0/0",
70+
err: errors.New("path must have 3 levels ie 0/0/0"),
71+
},
72+
"path too short should error": {
73+
path: "0/0",
74+
err: errors.New("path must have 3 levels ie 0/0/0"),
75+
},
76+
"path length 3 should not error": {
77+
path: "0/0/0",
78+
err: nil,
79+
},
80+
"path overflow uint32 should error": {
81+
path: "4294967296/0/0",
82+
err: &strconv.NumError{
83+
Func: "ParseUint",
84+
Num: "4294967296",
85+
Err: errors.New("value out of range"),
86+
},
87+
},
88+
"path less than min uint32 should error": {
89+
path: "-1/0/0",
90+
err: &strconv.NumError{
91+
Func: "ParseUint",
92+
Num: "-1",
93+
Err: errors.New("invalid syntax"),
94+
},
95+
},
96+
}
97+
for name, test := range tests {
98+
t.Run(name, func(t *testing.T) {
99+
_, err := DeriveNumber(test.path)
100+
assert.Equal(t, test.err, err)
101+
})
102+
}
103+
}

0 commit comments

Comments
 (0)