accounts, cmd, internal, node: implement HD wallet self-derivation
This commit is contained in:
parent
c5215fdd48
commit
205ea95802
@ -20,6 +20,7 @@ package accounts
|
|||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
|
ethereum "github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
@ -71,7 +72,19 @@ type Wallet interface {
|
|||||||
// Derive attempts to explicitly derive a hierarchical deterministic account at
|
// Derive attempts to explicitly derive a hierarchical deterministic account at
|
||||||
// the specified derivation path. If requested, the derived account will be added
|
// the specified derivation path. If requested, the derived account will be added
|
||||||
// to the wallet's tracked account list.
|
// to the wallet's tracked account list.
|
||||||
Derive(path string, pin bool) (Account, error)
|
Derive(path DerivationPath, pin bool) (Account, error)
|
||||||
|
|
||||||
|
// SelfDerive sets a base account derivation path from which the wallet attempts
|
||||||
|
// to discover non zero accounts and automatically add them to list of tracked
|
||||||
|
// accounts.
|
||||||
|
//
|
||||||
|
// Note, self derivaton will increment the last component of the specified path
|
||||||
|
// opposed to decending into a child path to allow discovering accounts starting
|
||||||
|
// from non zero components.
|
||||||
|
//
|
||||||
|
// You can disable automatic account discovery by calling SelfDerive with a nil
|
||||||
|
// chain state reader.
|
||||||
|
SelfDerive(base DerivationPath, chain ethereum.ChainStateReader)
|
||||||
|
|
||||||
// SignHash requests the wallet to sign the given hash.
|
// SignHash requests the wallet to sign the given hash.
|
||||||
//
|
//
|
||||||
|
130
accounts/hd.go
Normal file
130
accounts/hd.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// Copyright 2017 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultRootDerivationPath is the root path to which custom derivation endpoints
|
||||||
|
// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
|
||||||
|
// at m/44'/60'/0'/1, etc.
|
||||||
|
var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0}
|
||||||
|
|
||||||
|
// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
|
||||||
|
// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second
|
||||||
|
// at m/44'/60'/0'/1, etc.
|
||||||
|
var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
|
||||||
|
|
||||||
|
// DerivationPath represents the computer friendly version of a hierarchical
|
||||||
|
// deterministic wallet account derivaion path.
|
||||||
|
//
|
||||||
|
// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||||
|
// defines derivation paths to be of the form:
|
||||||
|
//
|
||||||
|
// m / purpose' / coin_type' / account' / change / address_index
|
||||||
|
//
|
||||||
|
// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
|
||||||
|
// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and
|
||||||
|
// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns
|
||||||
|
// the `coin_type` 60' (or 0x8000003C) to Ethereum.
|
||||||
|
//
|
||||||
|
// The root path for Ethereum is m/44'/60'/0'/0 according to the specification
|
||||||
|
// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone
|
||||||
|
// yet whether accounts should increment the last component or the children of
|
||||||
|
// that. We will go with the simpler approach of incrementing the last component.
|
||||||
|
type DerivationPath []uint32
|
||||||
|
|
||||||
|
// ParseDerivationPath converts a user specified derivation path string to the
|
||||||
|
// internal binary representation.
|
||||||
|
//
|
||||||
|
// Full derivation paths need to start with the `m/` prefix, relative derivation
|
||||||
|
// paths (which will get appended to the default root path) must not have prefixes
|
||||||
|
// in front of the first element. Whitespace is ignored.
|
||||||
|
func ParseDerivationPath(path string) (DerivationPath, error) {
|
||||||
|
var result DerivationPath
|
||||||
|
|
||||||
|
// Handle absolute or relative paths
|
||||||
|
components := strings.Split(path, "/")
|
||||||
|
switch {
|
||||||
|
case len(components) == 0:
|
||||||
|
return nil, errors.New("empty derivation path")
|
||||||
|
|
||||||
|
case strings.TrimSpace(components[0]) == "":
|
||||||
|
return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones")
|
||||||
|
|
||||||
|
case strings.TrimSpace(components[0]) == "m":
|
||||||
|
components = components[1:]
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = append(result, DefaultRootDerivationPath...)
|
||||||
|
}
|
||||||
|
// All remaining components are relative, append one by one
|
||||||
|
if len(components) == 0 {
|
||||||
|
return nil, errors.New("empty derivation path") // Empty relative paths
|
||||||
|
}
|
||||||
|
for _, component := range components {
|
||||||
|
// Ignore any user added whitespace
|
||||||
|
component = strings.TrimSpace(component)
|
||||||
|
var value uint32
|
||||||
|
|
||||||
|
// Handle hardened paths
|
||||||
|
if strings.HasSuffix(component, "'") {
|
||||||
|
value = 0x80000000
|
||||||
|
component = strings.TrimSpace(strings.TrimSuffix(component, "'"))
|
||||||
|
}
|
||||||
|
// Handle the non hardened component
|
||||||
|
bigval, ok := new(big.Int).SetString(component, 0)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid component: %s", component)
|
||||||
|
}
|
||||||
|
max := math.MaxUint32 - value
|
||||||
|
if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 {
|
||||||
|
if value == 0 {
|
||||||
|
return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max)
|
||||||
|
}
|
||||||
|
value += uint32(bigval.Uint64())
|
||||||
|
|
||||||
|
// Append and repeat
|
||||||
|
result = append(result, value)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the stringer interface, converting a binary derivation path
|
||||||
|
// to its canonical representation.
|
||||||
|
func (path DerivationPath) String() string {
|
||||||
|
result := "m"
|
||||||
|
for _, component := range path {
|
||||||
|
var hardened bool
|
||||||
|
if component >= 0x80000000 {
|
||||||
|
component -= 0x80000000
|
||||||
|
hardened = true
|
||||||
|
}
|
||||||
|
result = fmt.Sprintf("%s/%d", result, component)
|
||||||
|
if hardened {
|
||||||
|
result += "'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
79
accounts/hd_test.go
Normal file
79
accounts/hd_test.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2017 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests that HD derivation paths can be correctly parsed into our internal binary
|
||||||
|
// representation.
|
||||||
|
func TestHDPathParsing(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
output DerivationPath
|
||||||
|
}{
|
||||||
|
// Plain absolute derivation paths
|
||||||
|
{"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
{"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||||
|
{"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
{"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||||
|
{"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
{"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
|
||||||
|
// Plain relative derivation paths
|
||||||
|
{"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
{"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||||
|
{"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
{"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||||
|
{"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
|
||||||
|
// Hexadecimal absolute derivation paths
|
||||||
|
{"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
{"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||||
|
{"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
{"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||||
|
{"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
{"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
|
||||||
|
// Hexadecimal relative derivation paths
|
||||||
|
{"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
{"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||||
|
{"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
{"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||||
|
{"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||||
|
|
||||||
|
// Weird inputs just to ensure they work
|
||||||
|
{" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||||
|
|
||||||
|
// Invaid derivation paths
|
||||||
|
{"", nil}, // Empty relative derivation path
|
||||||
|
{"m", nil}, // Empty absolute derivation path
|
||||||
|
{"m/", nil}, // Missing last derivation component
|
||||||
|
{"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error
|
||||||
|
{"m/2147483648'", nil}, // Overflows 32 bit integer
|
||||||
|
{"m/-1'", nil}, // Cannot contain negative number
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) {
|
||||||
|
t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output)
|
||||||
|
} else if path == nil && err == nil {
|
||||||
|
t.Errorf("test %d: nil path and error: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ package keystore
|
|||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
|
ethereum "github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
)
|
)
|
||||||
@ -69,10 +70,14 @@ func (w *keystoreWallet) Contains(account accounts.Account) bool {
|
|||||||
|
|
||||||
// Derive implements accounts.Wallet, but is a noop for plain wallets since there
|
// Derive implements accounts.Wallet, but is a noop for plain wallets since there
|
||||||
// is no notion of hierarchical account derivation for plain keystore accounts.
|
// is no notion of hierarchical account derivation for plain keystore accounts.
|
||||||
func (w *keystoreWallet) Derive(path string, pin bool) (accounts.Account, error) {
|
func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
|
||||||
return accounts.Account{}, accounts.ErrNotSupported
|
return accounts.Account{}, accounts.ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since
|
||||||
|
// there is no notion of hierarchical account derivation for plain keystore accounts.
|
||||||
|
func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
|
||||||
|
|
||||||
// SignHash implements accounts.Wallet, attempting to sign the given hash with
|
// SignHash implements accounts.Wallet, attempting to sign the given hash with
|
||||||
// the given account. If the wallet does not wrap this particular account, an
|
// the given account. If the wallet does not wrap this particular account, an
|
||||||
// error is returned to avoid account leakage (even though in theory we may be
|
// error is returned to avoid account leakage (even though in theory we may be
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// +build !ios
|
|
||||||
|
|
||||||
package usbwallet
|
|
||||||
|
|
||||||
/*
|
|
||||||
func TestLedgerHub(t *testing.T) {
|
|
||||||
glog.SetV(6)
|
|
||||||
glog.SetToStderr(true)
|
|
||||||
|
|
||||||
// Create a USB hub watching for Ledger devices
|
|
||||||
hub, err := NewLedgerHub()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create Ledger hub: %v", err)
|
|
||||||
}
|
|
||||||
defer hub.Close()
|
|
||||||
|
|
||||||
// Wait for events :P
|
|
||||||
time.Sleep(time.Minute)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
func TestLedger(t *testing.T) {
|
|
||||||
// Create a USB context to access devices through
|
|
||||||
ctx, err := usb.NewContext()
|
|
||||||
defer ctx.Close()
|
|
||||||
ctx.Debug(6)
|
|
||||||
|
|
||||||
// List all of the Ledger wallets
|
|
||||||
wallets, err := findLedgerWallets(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to list Ledger wallets: %v", err)
|
|
||||||
}
|
|
||||||
// Retrieve the address from every one of them
|
|
||||||
for _, wallet := range wallets {
|
|
||||||
// Retrieve the version of the wallet app
|
|
||||||
ver, err := wallet.Version()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to retrieve wallet version: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Ledger version: %s\n", ver)
|
|
||||||
|
|
||||||
// Retrieve the address of the wallet
|
|
||||||
addr, err := wallet.Address()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to retrieve wallet address: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Ledger address: %x\n", addr)
|
|
||||||
|
|
||||||
// Try to sign a transaction with the wallet
|
|
||||||
unsigned := types.NewTransaction(1, common.HexToAddress("0xbabababababababababababababababababababa"), common.Ether, big.NewInt(20000), common.Shannon, nil)
|
|
||||||
signed, err := wallet.Sign(unsigned)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to sign transactions: %v", err)
|
|
||||||
}
|
|
||||||
signer, err := types.Sender(types.NewEIP155Signer(big.NewInt(1)), signed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recover signer: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Ledger signature by: %x\n", signer)
|
|
||||||
}
|
|
||||||
}*/
|
|
@ -29,11 +29,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ethereum "github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
@ -41,10 +40,15 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/logger/glog"
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/karalabe/gousb/usb"
|
"github.com/karalabe/gousb/usb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ledgerDerivationPath is the base derivation parameters used by the wallet.
|
// Maximum time between wallet health checks to detect USB unplugs.
|
||||||
var ledgerDerivationPath = []uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
|
const ledgerHeartbeatCycle = time.Second
|
||||||
|
|
||||||
|
// Minimum time to wait between self derivation attempts, even it the user is
|
||||||
|
// requesting accounts like crazy.
|
||||||
|
const ledgerSelfDeriveThrottling = time.Second
|
||||||
|
|
||||||
// ledgerOpcode is an enumeration encoding the supported Ledger opcodes.
|
// ledgerOpcode is an enumeration encoding the supported Ledger opcodes.
|
||||||
type ledgerOpcode byte
|
type ledgerOpcode byte
|
||||||
@ -84,7 +88,13 @@ type ledgerWallet struct {
|
|||||||
|
|
||||||
version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline)
|
version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline)
|
||||||
accounts []accounts.Account // List of derive accounts pinned on the Ledger
|
accounts []accounts.Account // List of derive accounts pinned on the Ledger
|
||||||
paths map[common.Address][]uint32 // Known derivation paths for signing operations
|
paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations
|
||||||
|
|
||||||
|
selfDeriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery
|
||||||
|
selfDeriveNextAddr common.Address // Next derived account address for auto-discovery
|
||||||
|
selfDerivePrevZero common.Address // Last zero-address where auto-discovery stopped
|
||||||
|
selfDeriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
|
||||||
|
selfDeriveTime time.Time // Timestamp of the last self-derivation to avoid thrashing
|
||||||
|
|
||||||
quit chan chan error
|
quit chan chan error
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
@ -107,12 +117,17 @@ func (w *ledgerWallet) Status() string {
|
|||||||
if w.device == nil {
|
if w.device == nil {
|
||||||
return "Closed"
|
return "Closed"
|
||||||
}
|
}
|
||||||
if w.version == [3]byte{0, 0, 0} {
|
if w.offline() {
|
||||||
return "Ethereum app offline"
|
return "Ethereum app offline"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
|
return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// offline returns whether the wallet and the Ethereum app is offline or not.
|
||||||
|
func (w *ledgerWallet) offline() bool {
|
||||||
|
return w.version == [3]byte{0, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
// Open implements accounts.Wallet, attempting to open a USB connection to the
|
// Open implements accounts.Wallet, attempting to open a USB connection to the
|
||||||
// Ledger hardware wallet. The Ledger does not require a user passphrase so that
|
// Ledger hardware wallet. The Ledger does not require a user passphrase so that
|
||||||
// is silently discarded.
|
// is silently discarded.
|
||||||
@ -176,13 +191,13 @@ func (w *ledgerWallet) Open(passphrase string) error {
|
|||||||
// Wallet seems to be successfully opened, guess if the Ethereum app is running
|
// Wallet seems to be successfully opened, guess if the Ethereum app is running
|
||||||
w.device, w.input, w.output = device, input, output
|
w.device, w.input, w.output = device, input, output
|
||||||
|
|
||||||
w.paths = make(map[common.Address][]uint32)
|
w.paths = make(map[common.Address]accounts.DerivationPath)
|
||||||
w.quit = make(chan chan error)
|
w.quit = make(chan chan error)
|
||||||
defer func() {
|
defer func() {
|
||||||
go w.heartbeat()
|
go w.heartbeat()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if _, err := w.deriveAddress(ledgerDerivationPath); err != nil {
|
if _, err := w.deriveAddress(accounts.DefaultBaseDerivationPath); err != nil {
|
||||||
// Ethereum app is not running, nothing more to do, return
|
// Ethereum app is not running, nothing more to do, return
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -209,7 +224,7 @@ func (w *ledgerWallet) heartbeat() {
|
|||||||
case errc = <-w.quit:
|
case errc = <-w.quit:
|
||||||
// Termination requested
|
// Termination requested
|
||||||
continue
|
continue
|
||||||
case <-time.After(time.Second):
|
case <-time.After(ledgerHeartbeatCycle):
|
||||||
// Heartbeat time
|
// Heartbeat time
|
||||||
}
|
}
|
||||||
// Execute a tiny data exchange to see responsiveness
|
// Execute a tiny data exchange to see responsiveness
|
||||||
@ -242,16 +257,86 @@ func (w *ledgerWallet) Close() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil
|
w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil
|
||||||
|
w.version = [3]byte{}
|
||||||
|
|
||||||
return herr // If all went well, return any health-check errors
|
return herr // If all went well, return any health-check errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accounts implements accounts.Wallet, returning the list of accounts pinned to
|
// Accounts implements accounts.Wallet, returning the list of accounts pinned to
|
||||||
// the Ledger hardware wallet.
|
// the Ledger hardware wallet. If self derivation was enabled, the account list
|
||||||
|
// is periodically expanded based on current chain state.
|
||||||
func (w *ledgerWallet) Accounts() []accounts.Account {
|
func (w *ledgerWallet) Accounts() []accounts.Account {
|
||||||
w.lock.RLock()
|
w.lock.Lock()
|
||||||
defer w.lock.RUnlock()
|
defer w.lock.Unlock()
|
||||||
|
|
||||||
|
// If the wallet is offline, there are no accounts to return
|
||||||
|
if w.offline() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// If no self derivation is done (or throttled), return the current accounts
|
||||||
|
if w.selfDeriveChain == nil || time.Since(w.selfDeriveTime) < ledgerSelfDeriveThrottling {
|
||||||
|
cpy := make([]accounts.Account, len(w.accounts))
|
||||||
|
copy(cpy, w.accounts)
|
||||||
|
return cpy
|
||||||
|
}
|
||||||
|
// Self derivation requested, try to expand our account list
|
||||||
|
ctx := context.Background()
|
||||||
|
for empty := false; !empty; {
|
||||||
|
// Retrieve the next derived Ethereum account
|
||||||
|
var err error
|
||||||
|
if w.selfDeriveNextAddr == (common.Address{}) {
|
||||||
|
w.selfDeriveNextAddr, err = w.deriveAddress(w.selfDeriveNextPath)
|
||||||
|
if err != nil {
|
||||||
|
// Derivation failed, disable auto discovery
|
||||||
|
glog.V(logger.Warn).Infof("self-derivation failed: %v", err)
|
||||||
|
w.selfDeriveChain = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check the account's status against the current chain state
|
||||||
|
balance, err := w.selfDeriveChain.BalanceAt(ctx, w.selfDeriveNextAddr, nil)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(logger.Warn).Infof("self-derivation balance retrieval failed: %v", err)
|
||||||
|
w.selfDeriveChain = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nonce, err := w.selfDeriveChain.NonceAt(ctx, w.selfDeriveNextAddr, nil)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(logger.Warn).Infof("self-derivation nonce retrieval failed: %v", err)
|
||||||
|
w.selfDeriveChain = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// If the next account is empty, stop self-derivation, but add it nonetheless
|
||||||
|
if balance.BitLen() == 0 && nonce == 0 {
|
||||||
|
w.selfDerivePrevZero = w.selfDeriveNextAddr
|
||||||
|
empty = true
|
||||||
|
}
|
||||||
|
// We've just self-derived a new non-zero account, start tracking it
|
||||||
|
path := make(accounts.DerivationPath, len(w.selfDeriveNextPath))
|
||||||
|
copy(path[:], w.selfDeriveNextPath[:])
|
||||||
|
|
||||||
|
account := accounts.Account{
|
||||||
|
Address: w.selfDeriveNextAddr,
|
||||||
|
URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
|
||||||
|
}
|
||||||
|
_, known := w.paths[w.selfDeriveNextAddr]
|
||||||
|
if !known || (!empty && w.selfDeriveNextAddr == w.selfDerivePrevZero) {
|
||||||
|
// Either fully new account, or previous zero. Report discovery either way
|
||||||
|
glog.V(logger.Info).Infof("%s discovered %s (balance %d, nonce %d) at %s", w.url.String(), w.selfDeriveNextAddr.Hex(), balance, nonce, path)
|
||||||
|
}
|
||||||
|
if !known {
|
||||||
|
w.accounts = append(w.accounts, account)
|
||||||
|
w.paths[w.selfDeriveNextAddr] = path
|
||||||
|
}
|
||||||
|
// Fetch the next potential account
|
||||||
|
if !empty {
|
||||||
|
w.selfDeriveNextAddr = common.Address{}
|
||||||
|
w.selfDeriveNextPath[len(w.selfDeriveNextPath)-1]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.selfDeriveTime = time.Now()
|
||||||
|
|
||||||
|
// Return whatever account list we ended up with
|
||||||
cpy := make([]accounts.Account, len(w.accounts))
|
cpy := make([]accounts.Account, len(w.accounts))
|
||||||
copy(cpy, w.accounts)
|
copy(cpy, w.accounts)
|
||||||
return cpy
|
return cpy
|
||||||
@ -271,34 +356,16 @@ func (w *ledgerWallet) Contains(account accounts.Account) bool {
|
|||||||
// Derive implements accounts.Wallet, deriving a new account at the specific
|
// Derive implements accounts.Wallet, deriving a new account at the specific
|
||||||
// derivation path. If pin is set to true, the account will be added to the list
|
// derivation path. If pin is set to true, the account will be added to the list
|
||||||
// of tracked accounts.
|
// of tracked accounts.
|
||||||
func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) {
|
func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
|
||||||
w.lock.Lock()
|
w.lock.Lock()
|
||||||
defer w.lock.Unlock()
|
defer w.lock.Unlock()
|
||||||
|
|
||||||
// If the wallet is closed, or the Ethereum app doesn't run, abort
|
// If the wallet is closed, or the Ethereum app doesn't run, abort
|
||||||
if w.device == nil || w.version == [3]byte{0, 0, 0} {
|
if w.device == nil || w.offline() {
|
||||||
return accounts.Account{}, accounts.ErrWalletClosed
|
return accounts.Account{}, accounts.ErrWalletClosed
|
||||||
}
|
}
|
||||||
// All seems fine, convert the user derivation path to Ledger representation
|
|
||||||
path = strings.TrimPrefix(path, "/")
|
|
||||||
|
|
||||||
parts := strings.Split(path, "/")
|
|
||||||
lpath := make([]uint32, len(parts))
|
|
||||||
for i, part := range parts {
|
|
||||||
// Handle hardened paths
|
|
||||||
if strings.HasSuffix(part, "'") {
|
|
||||||
lpath[i] = 0x80000000
|
|
||||||
part = strings.TrimSuffix(part, "'")
|
|
||||||
}
|
|
||||||
// Handle the non hardened component
|
|
||||||
val, err := strconv.Atoi(part)
|
|
||||||
if err != nil {
|
|
||||||
return accounts.Account{}, fmt.Errorf("path element %d: %v", i, err)
|
|
||||||
}
|
|
||||||
lpath[i] += uint32(val)
|
|
||||||
}
|
|
||||||
// Try to derive the actual account and update it's URL if succeeful
|
// Try to derive the actual account and update it's URL if succeeful
|
||||||
address, err := w.deriveAddress(lpath)
|
address, err := w.deriveAddress(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return accounts.Account{}, err
|
return accounts.Account{}, err
|
||||||
}
|
}
|
||||||
@ -310,12 +377,27 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) {
|
|||||||
if pin {
|
if pin {
|
||||||
if _, ok := w.paths[address]; !ok {
|
if _, ok := w.paths[address]; !ok {
|
||||||
w.accounts = append(w.accounts, account)
|
w.accounts = append(w.accounts, account)
|
||||||
w.paths[address] = lpath
|
w.paths[address] = path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelfDerive implements accounts.Wallet, trying to discover accounts that the
|
||||||
|
// user used previously (based on the chain state), but ones that he/she did not
|
||||||
|
// explicitly pin to the wallet manually. To avoid chain head monitoring, self
|
||||||
|
// derivation only runs during account listing (and even then throttled).
|
||||||
|
func (w *ledgerWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {
|
||||||
|
w.lock.Lock()
|
||||||
|
defer w.lock.Unlock()
|
||||||
|
|
||||||
|
w.selfDeriveNextPath = make(accounts.DerivationPath, len(base))
|
||||||
|
copy(w.selfDeriveNextPath[:], base[:])
|
||||||
|
|
||||||
|
w.selfDeriveNextAddr = common.Address{}
|
||||||
|
w.selfDeriveChain = chain
|
||||||
|
}
|
||||||
|
|
||||||
// SignHash implements accounts.Wallet, however signing arbitrary data is not
|
// SignHash implements accounts.Wallet, however signing arbitrary data is not
|
||||||
// supported for Ledger wallets, so this method will always return an error.
|
// supported for Ledger wallets, so this method will always return an error.
|
||||||
func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) {
|
func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) {
|
||||||
|
@ -25,12 +25,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/console"
|
"github.com/ethereum/go-ethereum/console"
|
||||||
"github.com/ethereum/go-ethereum/contracts/release"
|
"github.com/ethereum/go-ethereum/contracts/release"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
"github.com/ethereum/go-ethereum/internal/debug"
|
"github.com/ethereum/go-ethereum/internal/debug"
|
||||||
"github.com/ethereum/go-ethereum/logger"
|
"github.com/ethereum/go-ethereum/logger"
|
||||||
"github.com/ethereum/go-ethereum/logger/glog"
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
@ -249,12 +251,38 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
|||||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||||
|
|
||||||
passwords := utils.MakePasswordList(ctx)
|
passwords := utils.MakePasswordList(ctx)
|
||||||
accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
|
unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
|
||||||
for i, account := range accounts {
|
for i, account := range unlocks {
|
||||||
if trimmed := strings.TrimSpace(account); trimmed != "" {
|
if trimmed := strings.TrimSpace(account); trimmed != "" {
|
||||||
unlockAccount(ctx, ks, trimmed, i, passwords)
|
unlockAccount(ctx, ks, trimmed, i, passwords)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Register wallet event handlers to open and auto-derive wallets
|
||||||
|
events := make(chan accounts.WalletEvent, 16)
|
||||||
|
stack.AccountManager().Subscribe(events)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Create an chain state reader for self-derivation
|
||||||
|
rpcClient, err := stack.Attach()
|
||||||
|
if err != nil {
|
||||||
|
utils.Fatalf("Failed to attach to self: %v", err)
|
||||||
|
}
|
||||||
|
stateReader := ethclient.NewClient(rpcClient)
|
||||||
|
|
||||||
|
// Listen for wallet event till termination
|
||||||
|
for event := range events {
|
||||||
|
if event.Arrive {
|
||||||
|
if err := event.Wallet.Open(""); err != nil {
|
||||||
|
glog.V(logger.Info).Infof("New wallet appeared: %s, failed to open: %s", event.Wallet.URL(), err)
|
||||||
|
} else {
|
||||||
|
glog.V(logger.Info).Infof("New wallet appeared: %s, %s", event.Wallet.URL(), event.Wallet.Status())
|
||||||
|
}
|
||||||
|
event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
|
||||||
|
} else {
|
||||||
|
glog.V(logger.Info).Infof("Old wallet dropped: %s", event.Wallet.URL())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
// Start auxiliary services if enabled
|
// Start auxiliary services if enabled
|
||||||
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
|
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
|
||||||
var ethereum *eth.Ethereum
|
var ethereum *eth.Ethereum
|
||||||
|
@ -253,10 +253,14 @@ func (s *PrivateAccountAPI) DeriveAccount(url string, path string, pin *bool) (a
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return accounts.Account{}, err
|
return accounts.Account{}, err
|
||||||
}
|
}
|
||||||
|
derivPath, err := accounts.ParseDerivationPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return accounts.Account{}, err
|
||||||
|
}
|
||||||
if pin == nil {
|
if pin == nil {
|
||||||
pin = new(bool)
|
pin = new(bool)
|
||||||
}
|
}
|
||||||
return wallet.Derive(path, *pin)
|
return wallet.Derive(derivPath, *pin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAccount will create a new account and returns the address for the new account.
|
// NewAccount will create a new account and returns the address for the new account.
|
||||||
|
@ -446,22 +446,5 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
|
|||||||
} else {
|
} else {
|
||||||
backends = append(backends, ledgerhub)
|
backends = append(backends, ledgerhub)
|
||||||
}
|
}
|
||||||
am := accounts.NewManager(backends...)
|
return accounts.NewManager(backends...), ephemeral, nil
|
||||||
|
|
||||||
// Start some logging for the user
|
|
||||||
changes := make(chan accounts.WalletEvent, 16)
|
|
||||||
am.Subscribe(changes)
|
|
||||||
go func() {
|
|
||||||
for event := range changes {
|
|
||||||
if event.Arrive {
|
|
||||||
glog.V(logger.Info).Infof("New wallet appeared: %s", event.Wallet.URL())
|
|
||||||
if err := event.Wallet.Open(""); err != nil {
|
|
||||||
glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", event.Wallet.URL(), err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
glog.V(logger.Info).Infof("Old wallet disappeared: %s", event.Wallet.URL())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return am, ephemeral, nil
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user