Skip to content

Commit b71334a

Browse files
svenski123holiman
andauthored
accounts, signer: fix Ledger Live account derivation path (clef) (#21757)
* signer/core/api: fix derivation of ledger live accounts For ledger hardware wallets, change account iteration as follows: - ledger legacy: m/44'/60'/0'/X; for 0<=X<5 - ledger live: m/44'/60'/0'/0/X; for 0<=X<5 - ledger legacy: m/44'/60'/0'/X; for 0<=X<10 - ledger live: m/44'/60'/X'/0/0; for 0<=X<10 Non-ledger derivation is unchanged and remains as: - non-ledger: m/44'/60'/0'/0/X; for 0<=X<10 * signer/core/api: derive ten default paths for all hardware wallets, plus ten legacy and ten live paths for ledger wallets * signer/core/api: as .../0'/0/0 already included by default paths, do not include it again with ledger live paths * accounts, signer: implement path iterators for hd wallets Co-authored-by: Martin Holst Swende <martin@swende.se>
1 parent fa572cd commit b71334a

File tree

3 files changed

+117
-47
lines changed

3 files changed

+117
-47
lines changed

accounts/hd.go

+28
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,31 @@ func (path *DerivationPath) UnmarshalJSON(b []byte) error {
150150
*path, err = ParseDerivationPath(dp)
151151
return err
152152
}
153+
154+
// DefaultIterator creates a BIP-32 path iterator, which progresses by increasing the last component:
155+
// i.e. m/44'/60'/0'/0/0, m/44'/60'/0'/0/1, m/44'/60'/0'/0/2, ... m/44'/60'/0'/0/N.
156+
func DefaultIterator(base DerivationPath) func() DerivationPath {
157+
path := make(DerivationPath, len(base))
158+
copy(path[:], base[:])
159+
// Set it back by one, so the first call gives the first result
160+
path[len(path)-1]--
161+
return func() DerivationPath {
162+
path[len(path)-1]++
163+
return path
164+
}
165+
}
166+
167+
// LedgerLiveIterator creates a bip44 path iterator for Ledger Live.
168+
// Ledger Live increments the third component rather than the fifth component
169+
// i.e. m/44'/60'/0'/0/0, m/44'/60'/1'/0/0, m/44'/60'/2'/0/0, ... m/44'/60'/N'/0/0.
170+
func LedgerLiveIterator(base DerivationPath) func() DerivationPath {
171+
path := make(DerivationPath, len(base))
172+
copy(path[:], base[:])
173+
// Set it back by one, so the first call gives the first result
174+
path[2]--
175+
return func() DerivationPath {
176+
// ledgerLivePathIterator iterates on the third component
177+
path[2]++
178+
return path
179+
}
180+
}

accounts/hd_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package accounts
1818

1919
import (
20+
"fmt"
2021
"reflect"
2122
"testing"
2223
)
@@ -77,3 +78,41 @@ func TestHDPathParsing(t *testing.T) {
7778
}
7879
}
7980
}
81+
82+
func testDerive(t *testing.T, next func() DerivationPath, expected []string) {
83+
t.Helper()
84+
for i, want := range expected {
85+
if have := next(); fmt.Sprintf("%v", have) != want {
86+
t.Errorf("step %d, have %v, want %v", i, have, want)
87+
}
88+
}
89+
}
90+
91+
func TestHdPathIteration(t *testing.T) {
92+
testDerive(t, DefaultIterator(DefaultBaseDerivationPath),
93+
[]string{
94+
"m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1",
95+
"m/44'/60'/0'/0/2", "m/44'/60'/0'/0/3",
96+
"m/44'/60'/0'/0/4", "m/44'/60'/0'/0/5",
97+
"m/44'/60'/0'/0/6", "m/44'/60'/0'/0/7",
98+
"m/44'/60'/0'/0/8", "m/44'/60'/0'/0/9",
99+
})
100+
101+
testDerive(t, DefaultIterator(LegacyLedgerBaseDerivationPath),
102+
[]string{
103+
"m/44'/60'/0'/0", "m/44'/60'/0'/1",
104+
"m/44'/60'/0'/2", "m/44'/60'/0'/3",
105+
"m/44'/60'/0'/4", "m/44'/60'/0'/5",
106+
"m/44'/60'/0'/6", "m/44'/60'/0'/7",
107+
"m/44'/60'/0'/8", "m/44'/60'/0'/9",
108+
})
109+
110+
testDerive(t, LedgerLiveIterator(DefaultBaseDerivationPath),
111+
[]string{
112+
"m/44'/60'/0'/0/0", "m/44'/60'/1'/0/0",
113+
"m/44'/60'/2'/0/0", "m/44'/60'/3'/0/0",
114+
"m/44'/60'/4'/0/0", "m/44'/60'/5'/0/0",
115+
"m/44'/60'/6'/0/0", "m/44'/60'/7'/0/0",
116+
"m/44'/60'/8'/0/0", "m/44'/60'/9'/0/0",
117+
})
118+
}

signer/core/api.go

+50-47
Original file line numberDiff line numberDiff line change
@@ -322,62 +322,65 @@ func (api *SignerAPI) openTrezor(url accounts.URL) {
322322

323323
// startUSBListener starts a listener for USB events, for hardware wallet interaction
324324
func (api *SignerAPI) startUSBListener() {
325-
events := make(chan accounts.WalletEvent, 16)
325+
eventCh := make(chan accounts.WalletEvent, 16)
326326
am := api.am
327-
am.Subscribe(events)
328-
go func() {
327+
am.Subscribe(eventCh)
328+
// Open any wallets already attached
329+
for _, wallet := range am.Wallets() {
330+
if err := wallet.Open(""); err != nil {
331+
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
332+
if err == usbwallet.ErrTrezorPINNeeded {
333+
go api.openTrezor(wallet.URL())
334+
}
335+
}
336+
}
337+
go api.derivationLoop(eventCh)
338+
}
329339

330-
// Open any wallets already attached
331-
for _, wallet := range am.Wallets() {
332-
if err := wallet.Open(""); err != nil {
333-
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
340+
// derivationLoop listens for wallet events
341+
func (api *SignerAPI) derivationLoop(events chan accounts.WalletEvent) {
342+
// Listen for wallet event till termination
343+
for event := range events {
344+
switch event.Kind {
345+
case accounts.WalletArrived:
346+
if err := event.Wallet.Open(""); err != nil {
347+
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
334348
if err == usbwallet.ErrTrezorPINNeeded {
335-
go api.openTrezor(wallet.URL())
349+
go api.openTrezor(event.Wallet.URL())
336350
}
337351
}
338-
}
339-
// Listen for wallet event till termination
340-
for event := range events {
341-
switch event.Kind {
342-
case accounts.WalletArrived:
343-
if err := event.Wallet.Open(""); err != nil {
344-
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
345-
if err == usbwallet.ErrTrezorPINNeeded {
346-
go api.openTrezor(event.Wallet.URL())
352+
case accounts.WalletOpened:
353+
status, _ := event.Wallet.Status()
354+
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
355+
var derive = func(limit int, next func() accounts.DerivationPath) {
356+
// Derive first N accounts, hardcoded for now
357+
for i := 0; i < limit; i++ {
358+
path := next()
359+
if acc, err := event.Wallet.Derive(path, true); err != nil {
360+
log.Warn("Account derivation failed", "error", err)
361+
} else {
362+
log.Info("Derived account", "address", acc.Address, "path", path)
347363
}
348364
}
349-
case accounts.WalletOpened:
350-
status, _ := event.Wallet.Status()
351-
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
352-
var derive = func(numToDerive int, base accounts.DerivationPath) {
353-
// Derive first N accounts, hardcoded for now
354-
var nextPath = make(accounts.DerivationPath, len(base))
355-
copy(nextPath[:], base[:])
356-
357-
for i := 0; i < numToDerive; i++ {
358-
acc, err := event.Wallet.Derive(nextPath, true)
359-
if err != nil {
360-
log.Warn("Account derivation failed", "error", err)
361-
} else {
362-
log.Info("Derived account", "address", acc.Address, "path", nextPath)
363-
}
364-
nextPath[len(nextPath)-1]++
365-
}
366-
}
367-
if event.Wallet.URL().Scheme == "ledger" {
368-
log.Info("Deriving ledger default paths")
369-
derive(numberOfAccountsToDerive/2, accounts.DefaultBaseDerivationPath)
370-
log.Info("Deriving ledger legacy paths")
371-
derive(numberOfAccountsToDerive/2, accounts.LegacyLedgerBaseDerivationPath)
372-
} else {
373-
derive(numberOfAccountsToDerive, accounts.DefaultBaseDerivationPath)
374-
}
375-
case accounts.WalletDropped:
376-
log.Info("Old wallet dropped", "url", event.Wallet.URL())
377-
event.Wallet.Close()
378365
}
366+
log.Info("Deriving default paths")
367+
derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.DefaultBaseDerivationPath))
368+
if event.Wallet.URL().Scheme == "ledger" {
369+
log.Info("Deriving ledger legacy paths")
370+
derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.LegacyLedgerBaseDerivationPath))
371+
log.Info("Deriving ledger live paths")
372+
// For ledger live, since it's based off the same (DefaultBaseDerivationPath)
373+
// as one we've already used, we need to step it forward one step to avoid
374+
// hitting the same path again
375+
nextFn := accounts.LedgerLiveIterator(accounts.DefaultBaseDerivationPath)
376+
nextFn()
377+
derive(numberOfAccountsToDerive, nextFn)
378+
}
379+
case accounts.WalletDropped:
380+
log.Info("Old wallet dropped", "url", event.Wallet.URL())
381+
event.Wallet.Close()
379382
}
380-
}()
383+
}
381384
}
382385

383386
// List returns the set of wallet this signer manages. Each wallet can contain

0 commit comments

Comments
 (0)