Skip to content

Commit 205ea95

Browse files
committed
accounts, cmd, internal, node: implement HD wallet self-derivation
1 parent c5215fd commit 205ea95

File tree

9 files changed

+383
-136
lines changed

9 files changed

+383
-136
lines changed

accounts/accounts.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package accounts
2020
import (
2121
"math/big"
2222

23+
ethereum "github.com/ethereum/go-ethereum"
2324
"github.com/ethereum/go-ethereum/common"
2425
"github.com/ethereum/go-ethereum/core/types"
2526
"github.com/ethereum/go-ethereum/event"
@@ -71,7 +72,19 @@ type Wallet interface {
7172
// Derive attempts to explicitly derive a hierarchical deterministic account at
7273
// the specified derivation path. If requested, the derived account will be added
7374
// to the wallet's tracked account list.
74-
Derive(path string, pin bool) (Account, error)
75+
Derive(path DerivationPath, pin bool) (Account, error)
76+
77+
// SelfDerive sets a base account derivation path from which the wallet attempts
78+
// to discover non zero accounts and automatically add them to list of tracked
79+
// accounts.
80+
//
81+
// Note, self derivaton will increment the last component of the specified path
82+
// opposed to decending into a child path to allow discovering accounts starting
83+
// from non zero components.
84+
//
85+
// You can disable automatic account discovery by calling SelfDerive with a nil
86+
// chain state reader.
87+
SelfDerive(base DerivationPath, chain ethereum.ChainStateReader)
7588

7689
// SignHash requests the wallet to sign the given hash.
7790
//

accounts/hd.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2017 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package accounts
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"math"
23+
"math/big"
24+
"strings"
25+
)
26+
27+
// DefaultRootDerivationPath is the root path to which custom derivation endpoints
28+
// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
29+
// at m/44'/60'/0'/1, etc.
30+
var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0}
31+
32+
// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
33+
// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second
34+
// at m/44'/60'/0'/1, etc.
35+
var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
36+
37+
// DerivationPath represents the computer friendly version of a hierarchical
38+
// deterministic wallet account derivaion path.
39+
//
40+
// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
41+
// defines derivation paths to be of the form:
42+
//
43+
// m / purpose' / coin_type' / account' / change / address_index
44+
//
45+
// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
46+
// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and
47+
// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns
48+
// the `coin_type` 60' (or 0x8000003C) to Ethereum.
49+
//
50+
// The root path for Ethereum is m/44'/60'/0'/0 according to the specification
51+
// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone
52+
// yet whether accounts should increment the last component or the children of
53+
// that. We will go with the simpler approach of incrementing the last component.
54+
type DerivationPath []uint32
55+
56+
// ParseDerivationPath converts a user specified derivation path string to the
57+
// internal binary representation.
58+
//
59+
// Full derivation paths need to start with the `m/` prefix, relative derivation
60+
// paths (which will get appended to the default root path) must not have prefixes
61+
// in front of the first element. Whitespace is ignored.
62+
func ParseDerivationPath(path string) (DerivationPath, error) {
63+
var result DerivationPath
64+
65+
// Handle absolute or relative paths
66+
components := strings.Split(path, "/")
67+
switch {
68+
case len(components) == 0:
69+
return nil, errors.New("empty derivation path")
70+
71+
case strings.TrimSpace(components[0]) == "":
72+
return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones")
73+
74+
case strings.TrimSpace(components[0]) == "m":
75+
components = components[1:]
76+
77+
default:
78+
result = append(result, DefaultRootDerivationPath...)
79+
}
80+
// All remaining components are relative, append one by one
81+
if len(components) == 0 {
82+
return nil, errors.New("empty derivation path") // Empty relative paths
83+
}
84+
for _, component := range components {
85+
// Ignore any user added whitespace
86+
component = strings.TrimSpace(component)
87+
var value uint32
88+
89+
// Handle hardened paths
90+
if strings.HasSuffix(component, "'") {
91+
value = 0x80000000
92+
component = strings.TrimSpace(strings.TrimSuffix(component, "'"))
93+
}
94+
// Handle the non hardened component
95+
bigval, ok := new(big.Int).SetString(component, 0)
96+
if !ok {
97+
return nil, fmt.Errorf("invalid component: %s", component)
98+
}
99+
max := math.MaxUint32 - value
100+
if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 {
101+
if value == 0 {
102+
return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max)
103+
}
104+
return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max)
105+
}
106+
value += uint32(bigval.Uint64())
107+
108+
// Append and repeat
109+
result = append(result, value)
110+
}
111+
return result, nil
112+
}
113+
114+
// String implements the stringer interface, converting a binary derivation path
115+
// to its canonical representation.
116+
func (path DerivationPath) String() string {
117+
result := "m"
118+
for _, component := range path {
119+
var hardened bool
120+
if component >= 0x80000000 {
121+
component -= 0x80000000
122+
hardened = true
123+
}
124+
result = fmt.Sprintf("%s/%d", result, component)
125+
if hardened {
126+
result += "'"
127+
}
128+
}
129+
return result
130+
}

accounts/hd_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2017 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package accounts
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
)
23+
24+
// Tests that HD derivation paths can be correctly parsed into our internal binary
25+
// representation.
26+
func TestHDPathParsing(t *testing.T) {
27+
tests := []struct {
28+
input string
29+
output DerivationPath
30+
}{
31+
// Plain absolute derivation paths
32+
{"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
33+
{"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
34+
{"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
35+
{"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
36+
{"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
37+
{"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
38+
39+
// Plain relative derivation paths
40+
{"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
41+
{"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
42+
{"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
43+
{"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
44+
{"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
45+
46+
// Hexadecimal absolute derivation paths
47+
{"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
48+
{"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
49+
{"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
50+
{"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
51+
{"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
52+
{"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
53+
54+
// Hexadecimal relative derivation paths
55+
{"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
56+
{"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
57+
{"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
58+
{"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
59+
{"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
60+
61+
// Weird inputs just to ensure they work
62+
{" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
63+
64+
// Invaid derivation paths
65+
{"", nil}, // Empty relative derivation path
66+
{"m", nil}, // Empty absolute derivation path
67+
{"m/", nil}, // Missing last derivation component
68+
{"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error
69+
{"m/2147483648'", nil}, // Overflows 32 bit integer
70+
{"m/-1'", nil}, // Cannot contain negative number
71+
}
72+
for i, tt := range tests {
73+
if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) {
74+
t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output)
75+
} else if path == nil && err == nil {
76+
t.Errorf("test %d: nil path and error: %v", i, err)
77+
}
78+
}
79+
}

accounts/keystore/keystore_wallet.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package keystore
1919
import (
2020
"math/big"
2121

22+
ethereum "github.com/ethereum/go-ethereum"
2223
"github.com/ethereum/go-ethereum/accounts"
2324
"github.com/ethereum/go-ethereum/core/types"
2425
)
@@ -69,10 +70,14 @@ func (w *keystoreWallet) Contains(account accounts.Account) bool {
6970

7071
// Derive implements accounts.Wallet, but is a noop for plain wallets since there
7172
// is no notion of hierarchical account derivation for plain keystore accounts.
72-
func (w *keystoreWallet) Derive(path string, pin bool) (accounts.Account, error) {
73+
func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
7374
return accounts.Account{}, accounts.ErrNotSupported
7475
}
7576

77+
// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since
78+
// there is no notion of hierarchical account derivation for plain keystore accounts.
79+
func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
80+
7681
// SignHash implements accounts.Wallet, attempting to sign the given hash with
7782
// the given account. If the wallet does not wrap this particular account, an
7883
// error is returned to avoid account leakage (even though in theory we may be

accounts/usbwallet/ledger_test.go

Lines changed: 0 additions & 77 deletions
This file was deleted.

0 commit comments

Comments
 (0)