From fe0d7ac4cd4c91e88ba927bc246f883323c6a238 Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Sat, 7 Jul 2018 16:57:28 -0700 Subject: [PATCH] wip implementation of account.Wallet --- Gopkg.lock | 2 +- Makefile | 4 + README.md | 4 +- example/example.go | 31 ++-- hdwallet.go | 406 ++++++++++++++++++++++++++++++++------------- hdwallet_test.go | 90 ++++++++-- 6 files changed, 387 insertions(+), 150 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 357eb9c..2636db9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -126,6 +126,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "0fd9e0fe3ab2cd87e21b66400cbe2d84703f75bad7e222162ea6d9b60ed96998" + inputs-digest = "bffb815f162c96c8cb351af8d90b29f6e556d91968a2c7b2fe94c79bcdebf068" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index 8615684..f2871e5 100644 --- a/Makefile +++ b/Makefile @@ -12,3 +12,7 @@ test: .PHONY: deps/cp deps/cp: @cp -r "${GOPATH}/src/github.com/ethereum/go-ethereum/crypto/secp256k1/libsecp256k1" "vendor/github.com/ethereum/go-ethereum/crypto/secp256k1/" + +.PHONY: example +example: + @go run -v example/example.go diff --git a/README.md b/README.md index 0e59ce2..97e020f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # go-ethereum-hdwallet -> Ethereum HD Wallet derivations from mnemonic in Go (golang) - -Note: This is a WIP. Although it's working, I'm going to implement the `Wallet` interface from go-ethereum which will break the current API. +> Ethereum HD Wallet derivations from mnemonic in Go (golang). Implements the [go-ethereum](https://github.com/ethereum/go-ethereum) [`accounts.Wallet`](https://github.com/ethereum/go-ethereum/blob/master/accounts/accounts.go) interface. ## Install diff --git a/example/example.go b/example/example.go index e8cf5ab..ad37778 100644 --- a/example/example.go +++ b/example/example.go @@ -2,23 +2,32 @@ package main import ( "fmt" + "log" - "github.com/miguelmota/go-ethereum-hdwallet" + //"github.com/miguelmota/go-ethereum-hdwallet" + hdwallet ".." ) func main() { mnemonic := "tag volcano eight thank tide danger coast health above argue embrace heavy" - root, _ := hdwallet.New(&hdwallet.Config{ - Mnemonic: mnemonic, - Path: `m/44'/60'/0'/0`, - }) + wallet, err := hdwallet.NewFromMnemonic(mnemonic) + if err != nil { + log.Fatal(err) + } - wallet0, _ := root.Derive(0) - fmt.Println(wallet0.AddressHex()) // 0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947 + path := hdwallet.MustParseDerivationPath("m/44'/60'/0'/0/0") + account, err := wallet.Derive(path, false) + if err != nil { + log.Fatal(err) + } - wallet1, _ := root.Derive(1) - fmt.Println(wallet1.AddressHex()) // 0x8230645aC28A4EdD1b0B53E7Cd8019744E9dD559 + fmt.Println(account.Address.Hex()) // 0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947 - wallet2, _ := root.Derive(2) - fmt.Println(wallet2.AddressHex()) // 0x65c150B7eF3B1adbB9cB2b8041C892b15eDde05A + path = hdwallet.MustParseDerivationPath("m/44'/60'/0'/0/1") + account, err = wallet.Derive(path, false) + if err != nil { + log.Fatal(err) + } + + fmt.Println(account.Address.Hex()) // 0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947 } diff --git a/hdwallet.go b/hdwallet.go index 9013df7..0f61d35 100644 --- a/hdwallet.go +++ b/hdwallet.go @@ -3,13 +3,16 @@ package hdwallet import ( "crypto/ecdsa" "errors" - "fmt" + "math/big" + "sync" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/tyler-smith/go-bip32" "github.com/tyler-smith/go-bip39" @@ -17,180 +20,305 @@ import ( // Wallet ... type Wallet struct { - mnemonic string - path string - root *hdkeychain.ExtendedKey - extendedKey *hdkeychain.ExtendedKey - privateKey *ecdsa.PrivateKey - publicKey *ecdsa.PublicKey + mnemonic string + masterKey *hdkeychain.ExtendedKey + seed []byte + url accounts.URL + paths map[common.Address]accounts.DerivationPath + accounts []accounts.Account + stateLock sync.RWMutex } -// Config ... -type Config struct { - Mnemonic string - Path string -} - -// New ... -func New(config *Config) (*Wallet, error) { - if config.Path == "" { - config.Path = `m/44'/60'/0'/0` - } - - if config.Mnemonic == "" { +// NewFromMnemonic ... +func NewFromMnemonic(mnemonic string) (*Wallet, error) { + if mnemonic == "" { return nil, errors.New("mnemonic is required") } - seed := bip39.NewSeed(config.Mnemonic, "") - dpath, err := accounts.ParseDerivationPath(config.Path) - if err != nil { - return nil, err - } + seed := bip39.NewSeed(mnemonic, "") masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) if err != nil { return nil, err } - key := masterKey - - for _, n := range dpath { - key, err = key.Child(n) - if err != nil { - return nil, err - } + wallet := &Wallet{ + mnemonic: mnemonic, + seed: seed, + masterKey: masterKey, + accounts: []accounts.Account{}, + paths: map[common.Address]accounts.DerivationPath{}, } - privateKey, err := key.ECPrivKey() - privateKeyECDSA := privateKey.ToECDSA() + return wallet, nil +} + +// URL implements accounts.Wallet, returning the URL of the USB hardware device, however this does nothing since this is not a USB device. +func (w *Wallet) URL() accounts.URL { + return w.url +} + +// Status implements accounts.Wallet, returning a custom status message from the +// underlying vendor-specific hardware wallet implementation. +func (w *Wallet) Status() (string, error) { + return "ok", nil +} + +// Open implements the accounts wallet Close function. Since this is not a USB device, this methods does nothing. +func (w *Wallet) Open(passphrase string) error { + return nil +} + +// Close implements the accounts wallet Close function. Since this is not a USB device, this methods does nothing. +func (w *Wallet) Close() error { + return nil +} + +// Accounts implements accounts.Wallet, returning the list of accounts pinned to +// the wallet. If self-derivation was enabled, the account list is +// periodically expanded based on current chain state. +func (w *Wallet) Accounts() []accounts.Account { + // Attempt self-derivation if it's running + // Return whatever account list we ended up with + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy +} + +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not pinned into this wallet instance. +func (w *Wallet) Contains(account accounts.Account) bool { + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + _, exists := w.paths[account.Address] + return exists +} + +// 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 +// of tracked accounts. +func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { + // Try to derive the actual account and update its URL if successful + w.stateLock.RLock() // Avoid device disappearing during derivation + + address, err := w.deriveAddress(path) + + w.stateLock.RUnlock() + + // If an error occurred or no pinning was requested, return if err != nil { - return nil, err + return accounts.Account{}, err } - publicKey := privateKeyECDSA.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) - if !ok { - return nil, errors.New("failed ot get public key") + account := accounts.Account{ + Address: address, + URL: accounts.URL{ + Scheme: "", + Path: path.String(), + }, } - wallet := &Wallet{ - mnemonic: config.Mnemonic, - path: config.Path, - root: masterKey, - extendedKey: key, - privateKey: privateKeyECDSA, - publicKey: publicKeyECDSA, + if !pin { + return account, nil } - return wallet, nil -} + // Pinning needs to modify the state + w.stateLock.Lock() + defer w.stateLock.Unlock() -// Derive ... -func (s Wallet) Derive(index interface{}) (*Wallet, error) { - var idx uint32 - switch v := index.(type) { - case int: - idx = uint32(v) - case int8: - idx = uint32(v) - case int16: - idx = uint32(v) - case int32: - idx = uint32(v) - case int64: - idx = uint32(v) - case uint: - idx = uint32(v) - case uint8: - idx = uint32(v) - case uint16: - idx = uint32(v) - case uint32: - idx = v - case uint64: - idx = uint32(v) - default: - return nil, errors.New("unsupported index type") - } - - address, err := s.extendedKey.Child(idx) - if err != nil { - return nil, err + if _, ok := w.paths[address]; !ok { + w.accounts = append(w.accounts, account) + w.paths[address] = path } - privateKey, err := address.ECPrivKey() - privateKeyECDSA := privateKey.ToECDSA() - if err != nil { - return nil, err - } + return account, nil +} - publicKey := privateKeyECDSA.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) +// 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 *Wallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) { + /* + w.stateLock.Lock() + defer w.stateLock.Unlock() + + w.deriveNextPath = make(accounts.DerivationPath, len(base)) + copy(w.deriveNextPath[:], base[:]) + + w.deriveNextAddr = common.Address{} + w.deriveChain = chain + */ +} + +// SignHash implements accounts.Wallet which allows signing of arbitrary data +func (w *Wallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { + return nil, nil + +} + +// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger +// wallet to request a confirmation from the user. It returns either the signed +// transaction or a failure if the user denied the transaction. +// +// Note, if the version of the Ethereum application running on the Ledger wallet is +// too old to sign EIP-155 transactions, but such is requested nonetheless, an error +// will be returned opposed to silently signing in Homestead mode. +func (w *Wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + w.stateLock.RLock() // Comms have own mutex, this is for the state fields + defer w.stateLock.RUnlock() + + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] if !ok { - return nil, errors.New("failed ot get public key") + return nil, accounts.ErrUnknownAccount } - path := fmt.Sprintf("%s/%v", s.path, idx) + // Sign the transaction and verify the sender to avoid hardware fault surprises + /* + sender, signed, err := w.signTx(path, tx, chainID) + if err != nil { + return nil, err + } + if sender != account.Address { + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex()) + } + return signed, nil + */ + _ = path + return nil, nil +} - wallet := &Wallet{ - path: path, - root: s.extendedKey, - extendedKey: address, - privateKey: privateKeyECDSA, - publicKey: publicKeyECDSA, - } +// SignHashWithPassphrase implements accounts.Wallet +func (w *Wallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + return nil, nil +} - return wallet, nil +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +// Since USB wallets don't rely on passphrases, these are silently ignored. +func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return w.SignTx(account, tx, chainID) +} + +// Mnemonic ... +func (w *Wallet) Mnemonic() (string, error) { + if w.mnemonic == "" { + return "", errors.New("mnemonic not found") + } + return w.mnemonic, nil } // PrivateKey ... -func (s Wallet) PrivateKey() *ecdsa.PrivateKey { - return s.privateKey +func (w *Wallet) PrivateKey(account accounts.Account) (*ecdsa.PrivateKey, error) { + path, err := ParseDerivationPath(account.URL.Path) + if err != nil { + return nil, err + } + + return w.derivePrivateKey(path) } // PrivateKeyBytes ... -func (s Wallet) PrivateKeyBytes() []byte { - return crypto.FromECDSA(s.PrivateKey()) +func (w *Wallet) PrivateKeyBytes(account accounts.Account) ([]byte, error) { + privateKey, err := w.PrivateKey(account) + if err != nil { + return nil, err + } + + return crypto.FromECDSA(privateKey), nil } // PrivateKeyHex ... -func (s Wallet) PrivateKeyHex() string { - return hexutil.Encode(s.PrivateKeyBytes())[2:] +func (w *Wallet) PrivateKeyHex(account accounts.Account) (string, error) { + privateKeyBytes, err := w.PrivateKeyBytes(account) + if err != nil { + return "", err + } + + return hexutil.Encode(privateKeyBytes)[2:], nil } // PublicKey ... -func (s Wallet) PublicKey() *ecdsa.PublicKey { - return s.publicKey +func (w *Wallet) PublicKey(account accounts.Account) (*ecdsa.PublicKey, error) { + path, err := ParseDerivationPath(account.URL.Path) + if err != nil { + return nil, err + } + + return w.derivePublicKey(path) } // PublicKeyBytes ... -func (s Wallet) PublicKeyBytes() []byte { - return crypto.FromECDSAPub(s.PublicKey()) +func (w *Wallet) PublicKeyBytes(account accounts.Account) ([]byte, error) { + publicKey, err := w.PublicKey(account) + if err != nil { + return nil, err + } + + return crypto.FromECDSAPub(publicKey), nil } // PublicKeyHex ... -func (s Wallet) PublicKeyHex() string { - return hexutil.Encode(s.PublicKeyBytes())[4:] +func (w *Wallet) PublicKeyHex(account accounts.Account) (string, error) { + publicKeyBytes, err := w.PublicKeyBytes(account) + if err != nil { + return "", err + } + + return hexutil.Encode(publicKeyBytes)[4:], nil } // Address ... -func (s Wallet) Address() common.Address { - return crypto.PubkeyToAddress(*s.publicKey) +func (w *Wallet) Address(account accounts.Account) (common.Address, error) { + publicKey, err := w.PublicKey(account) + if err != nil { + return common.Address{}, err + } + + return crypto.PubkeyToAddress(*publicKey), nil +} + +// AddressBytes ... +func (w *Wallet) AddressBytes(account accounts.Account) ([]byte, error) { + address, err := w.Address(account) + if err != nil { + return nil, err + } + return address.Bytes(), nil } // AddressHex ... -func (s Wallet) AddressHex() string { - return s.Address().Hex() +func (w *Wallet) AddressHex(account accounts.Account) (string, error) { + address, err := w.Address(account) + if err != nil { + return "", err + } + return address.Hex(), nil } // Path ... -func (s Wallet) Path() string { - return s.path +func (w *Wallet) Path(account accounts.Account) (string, error) { + return account.URL.Path, nil } -// Mnemonic ... -func (s Wallet) Mnemonic() string { - return s.mnemonic +// ParseDerivationPath ... +func ParseDerivationPath(path string) (accounts.DerivationPath, error) { + return accounts.ParseDerivationPath(path) +} + +// MustParseDerivationPath ... +func MustParseDerivationPath(path string) accounts.DerivationPath { + parsed, err := accounts.ParseDerivationPath(path) + if err != nil { + panic(err) + } + + return parsed } // NewMnemonic ... @@ -206,3 +334,47 @@ func NewMnemonic() (string, error) { func NewSeed() ([]byte, error) { return bip32.NewSeed() } + +func (w *Wallet) derivePrivateKey(path accounts.DerivationPath) (*ecdsa.PrivateKey, error) { + var err error + key := w.masterKey + for _, n := range path { + key, err = key.Child(n) + if err != nil { + return nil, err + } + } + + privateKey, err := key.ECPrivKey() + privateKeyECDSA := privateKey.ToECDSA() + if err != nil { + return nil, err + } + + return privateKeyECDSA, nil +} + +func (w *Wallet) derivePublicKey(path accounts.DerivationPath) (*ecdsa.PublicKey, error) { + privateKeyECDSA, err := w.derivePrivateKey(path) + if err != nil { + return nil, err + } + + publicKey := privateKeyECDSA.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return nil, errors.New("failed to get public key") + } + + return publicKeyECDSA, nil +} + +func (w *Wallet) deriveAddress(path accounts.DerivationPath) (common.Address, error) { + publicKeyECDSA, err := w.derivePublicKey(path) + if err != nil { + return common.Address{}, err + } + + address := crypto.PubkeyToAddress(*publicKeyECDSA) + return address, nil +} diff --git a/hdwallet_test.go b/hdwallet_test.go index 6b59d6f..6f07e94 100644 --- a/hdwallet_test.go +++ b/hdwallet_test.go @@ -6,50 +6,104 @@ import ( // TODO: table test -func TestNew(t *testing.T) { +func TestWallet(t *testing.T) { mnemonic := "tag volcano eight thank tide danger coast health above argue embrace heavy" - root, err := New(&Config{ - Mnemonic: mnemonic, - Path: "m/44'/60'/0'/0", - }) + wallet, err := NewFromMnemonic(mnemonic) if err != nil { t.Error(err) } - if root.PrivateKeyHex() != "7657783b9ba4d4b16062337235432bbc5c80e3dce39fdc91e62d744fdb665cad" { - t.Error("wrong private key") + walletmnemonic, err := wallet.Mnemonic() + if err != nil { + t.Error(err) } - if root.PublicKeyHex() != "177c0776ca4c9e160822a1006eb6d236039eb882da8d7687ba20049d73e6230cae699eb8037aeeee2098d433d4210401a0cc1bf635c3fee2a40933d22c1206e7" { - t.Error("wrong public key") + if walletmnemonic != mnemonic { + t.Error("wrong mnemonic") + } + + path, err := ParseDerivationPath("m/44'/60'/0'/0/0") + if err != nil { + t.Error(err) + } + + account, err := wallet.Derive(path, false) + if err != nil { + t.Error(err) } - if root.AddressHex() != "0xAF1c991f6068Ac832eC60A8557eF1C7D8B9BcCD6" { + if account.Address.Hex() != "0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947" { t.Error("wrong address") } - if root.Path() != `m/44'/60'/0'/0` { + if len(wallet.Accounts()) != 0 { + t.Error("expected 0") + } + + account, err = wallet.Derive(path, true) + if err != nil { + t.Error(err) + } + + if len(wallet.Accounts()) != 1 { + t.Error("expected 1") + } + + url := wallet.URL() + if url.String() != "" { + t.Error("expected empty url") + } + + if err := wallet.Open(""); err != nil { + t.Error(err) + } + + if err := wallet.Close(); err != nil { + t.Error(err) + } + + status, err := wallet.Status() + if err != nil { + t.Error(err) + } + + if status != "ok" { + t.Error("expected status ok") + } + + accountPath, err := wallet.Path(account) + if err != nil { + t.Error(err) + } + + if accountPath != `m/44'/60'/0'/0/0` { t.Error("wrong hdpath") } - wallet, err := root.Derive(0) + privateKeyHex, err := wallet.PrivateKeyHex(account) if err != nil { t.Error(err) } - if wallet.PrivateKeyHex() != "63e21d10fd50155dbba0e7d3f7431a400b84b4c2ac1ee38872f82448fe3ecfb9" { + if privateKeyHex != "63e21d10fd50155dbba0e7d3f7431a400b84b4c2ac1ee38872f82448fe3ecfb9" { t.Error("wrong private key") } - if wallet.PublicKeyHex() != "6005c86a6718f66221713a77073c41291cc3abbfcd03aa4955e9b2b50dbf7f9b6672dad0d46ade61e382f79888a73ea7899d9419becf1d6c9ec2087c1188fa18" { + publicKeyHex, err := wallet.PublicKeyHex(account) + if err != nil { + t.Error(err) + } + + if publicKeyHex != "6005c86a6718f66221713a77073c41291cc3abbfcd03aa4955e9b2b50dbf7f9b6672dad0d46ade61e382f79888a73ea7899d9419becf1d6c9ec2087c1188fa18" { t.Error("wrong public key") } - if wallet.AddressHex() != "0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947" { - t.Error("wrong address") + addressHex, err := wallet.AddressHex(account) + if err != nil { + t.Error(err) } - if wallet.Path() != `m/44'/60'/0'/0/0` { - t.Error("wrong hdpath") + if addressHex != "0xC49926C4124cEe1cbA0Ea94Ea31a6c12318df947" { + t.Error("wrong address") } }