BIP44-compliant CKD + sending transactions w/o unlocking

This commit is contained in:
Victor Farazdagi 2016-08-21 09:45:59 +03:00
parent e403fec849
commit 3b0f776d1d
11 changed files with 471 additions and 143 deletions

View File

@ -53,6 +53,11 @@ const (
// fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes
// public/private key data.
serializedKeyLen = 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes
CoinTypeBTC = 0 // 0x80000000
CoinTypeTestNet = 1 // 0x80000001
CoinTypeETH = 60 // 0x8000003c
CoinTypeETC = 60 // 0x80000000
)
var (
@ -63,11 +68,15 @@ var (
ErrDerivingHardenedFromPublic = errors.New("cannot derive a hardened key from public key")
ErrBadChecksum = errors.New("bad extended key checksum")
ErrInvalidKeyLen = errors.New("serialized extended key length is invalid")
ErrDerivingChild = errors.New("error deriving child key")
ErrInvalidMasterKey = errors.New("invalid master key supplied")
PrivateKeyVersion, _ = hex.DecodeString("0488ADE4")
PublicKeyVersion, _ = hex.DecodeString("0488B21E")
)
type CoinType int
type ExtendedKey struct {
Version []byte // 4 bytes, mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private
Depth uint16 // 1 byte, depth: 0x00 for master nodes, 0x01 for level-1 derived keys, ....
@ -184,6 +193,45 @@ func (parent *ExtendedKey) Child(i uint32) (*ExtendedKey, error) {
return child, nil
}
// Child1 returns Status CKD#1 (used for ETH and SHH).
// BIP44 format is used: m / purpose' / coin_type' / account' / change / address_index
func (master *ExtendedKey) BIP44Child(coinType, i uint32) (*ExtendedKey, error) {
if !master.IsPrivate {
return nil, ErrInvalidMasterKey
}
if master.Depth != 0 {
return nil, ErrInvalidMasterKey
}
// m/44'/60'/0'/0/index
extKey, err := master.Derive([]uint32{
HardenedKeyStart + 44, // purpose
HardenedKeyStart + coinType, // cointype
HardenedKeyStart + 0, // account
0, // 0 - public, 1 - private
i, // index
})
if err != nil {
return nil, err
}
return extKey, nil
}
func (parent *ExtendedKey) Derive(path []uint32) (*ExtendedKey, error) {
var err error
extKey := parent
for _, i := range path {
extKey, err = extKey.Child(i)
if err != nil {
return nil, ErrDerivingChild
}
}
return extKey, nil
}
func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
// Already an extended public key.
if !k.IsPrivate {

View File

@ -12,7 +12,6 @@ import (
)
func TestBIP32Vectors(t *testing.T) {
hkStart := uint32(0x80000000)
tests := []struct {
name string
seed string
@ -31,35 +30,35 @@ func TestBIP32Vectors(t *testing.T) {
{
"test vector 1 chain m/0H",
"000102030405060708090a0b0c0d0e0f",
[]uint32{hkStart},
[]uint32{extkeys.HardenedKeyStart},
"xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw",
"xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7",
},
{
"test vector 1 chain m/0H/1",
"000102030405060708090a0b0c0d0e0f",
[]uint32{hkStart, 1},
[]uint32{extkeys.HardenedKeyStart, 1},
"xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ",
"xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs",
},
{
"test vector 1 chain m/0H/1/2H",
"000102030405060708090a0b0c0d0e0f",
[]uint32{hkStart, 1, hkStart + 2},
[]uint32{extkeys.HardenedKeyStart, 1, extkeys.HardenedKeyStart + 2},
"xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5",
"xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM",
},
{
"test vector 1 chain m/0H/1/2H/2",
"000102030405060708090a0b0c0d0e0f",
[]uint32{hkStart, 1, hkStart + 2, 2},
[]uint32{extkeys.HardenedKeyStart, 1, extkeys.HardenedKeyStart + 2, 2},
"xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV",
"xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334",
},
{
"test vector 1 chain m/0H/1/2H/2/1000000000",
"000102030405060708090a0b0c0d0e0f",
[]uint32{hkStart, 1, hkStart + 2, 2, 1000000000},
[]uint32{extkeys.HardenedKeyStart, 1, extkeys.HardenedKeyStart + 2, 2, 1000000000},
"xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy",
"xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76",
},
@ -81,28 +80,28 @@ func TestBIP32Vectors(t *testing.T) {
{
"test vector 2 chain m/0/2147483647H",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, hkStart + 2147483647},
[]uint32{0, extkeys.HardenedKeyStart + 2147483647},
"xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a",
"xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9",
},
{
"test vector 2 chain m/0/2147483647H/1",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, hkStart + 2147483647, 1},
[]uint32{0, extkeys.HardenedKeyStart + 2147483647, 1},
"xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon",
"xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef",
},
{
"test vector 2 chain m/0/2147483647H/1/2147483646H",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646},
[]uint32{0, extkeys.HardenedKeyStart + 2147483647, 1, extkeys.HardenedKeyStart + 2147483646},
"xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",
"xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc",
},
{
"test vector 2 chain m/0/2147483647H/1/2147483646H/2",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646, 2},
[]uint32{0, extkeys.HardenedKeyStart + 2147483647, 1, extkeys.HardenedKeyStart + 2147483646, 2},
"xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt",
"xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j",
},
@ -127,12 +126,10 @@ tests:
continue
}
for _, ind := range test.path {
extKey, err = extKey.Child(ind)
if err != nil {
t.Errorf("cannot derive child: %v", err)
continue tests
}
extKey, err = extKey.Derive(test.path)
if err != nil {
t.Errorf("cannot derive child: %v", err)
continue tests
}
privKeyStr := extKey.String()
@ -271,16 +268,10 @@ tests:
t.Errorf("NewKeyFromString #%d (%s): unexpected error creating extended key: %v", i, test.name, err)
continue
}
for _, childNum := range test.path {
var err error
extKey, err = extKey.Child(childNum)
if err != nil {
t.Errorf("err: %v", err)
continue tests
}
t.Logf("test %d (%s): %s", i, test.name, extKey.String())
extKey, err = extKey.Derive(test.path)
if err != nil {
t.Errorf("cannot derive child: %v", err)
continue tests
}
privStr := extKey.String()
@ -288,6 +279,8 @@ tests:
t.Errorf("Child #%d (%s): mismatched serialized private extended key -- got: %s, want: %s",
i, test.name, privStr, test.wantPriv)
continue
} else {
t.Logf("test %d (%s): %s", i, test.name, extKey.String())
}
}
@ -391,21 +384,18 @@ tests:
continue
}
for _, childNum := range test.path {
var err error
extKey, err = extKey.Child(childNum)
if err != nil {
t.Errorf("err: %v", err)
continue tests
}
t.Logf("test %d (%s): %s", i, test.name, extKey.String())
extKey, err = extKey.Derive(test.path)
if err != nil {
t.Errorf("cannot derive child: %v", err)
continue tests
}
pubStr := extKey.String()
if pubStr != test.wantPub {
t.Errorf("Child #%d (%s): mismatched serialized public extended key -- got: %s, want: %s", i, test.name, pubStr, test.wantPub)
continue
} else {
t.Logf("test %d (%s): %s", i, test.name, extKey.String())
}
}
}
@ -448,6 +438,17 @@ func TestErrors(t *testing.T) {
t.Errorf("Child: mismatched error -- got: %v, want: %v", err, extkeys.ErrDerivingHardenedFromPublic)
}
_, err = pubKey.BIP44Child(extkeys.CoinTypeETH, 0)
if err != extkeys.ErrInvalidMasterKey {
t.Errorf("BIP44Child: mistmatched error -- got: %v, want: %v", err, extkeys.ErrInvalidMasterKey)
}
childKey, _ := extKey.Child(extkeys.HardenedKeyStart + 1)
_, err = childKey.BIP44Child(extkeys.CoinTypeETH, 0) // this should be called from master only
if err != extkeys.ErrInvalidMasterKey {
t.Errorf("BIP44Child: mistmatched error -- got: %v, want: %v", err, extkeys.ErrInvalidMasterKey)
}
// NewKeyFromString failure tests.
tests := []struct {
name string
@ -497,6 +498,35 @@ func TestErrors(t *testing.T) {
}
}
func TestBIP44ChildDerivation(t *testing.T) {
keyString := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
derivedKey1String := "xprvA38t8tFW4vbuB7WJXEqMFmZqRrcZUKWqqMcGjjKjr2hbfvPhRtLLJGL4ayWG8shF1VkuUikVGodGshLiKRS7WrdsrGSVDQCY33qoPBxG2Kp"
derivedKey2String := "xprvA38t8tFW4vbuDgBNpekPnuMSfpWziDLdF7W9Zd3mPy6eDEkM5F17vk59RtVoFbNdBBq84EJf5CqdZhhEoBkAM4DXHQsDqvUxVnncfnDQEFg"
extKey, err := extkeys.NewKeyFromString(keyString)
if err != nil {
t.Errorf("NewKeyFromString: cannot create extended key")
}
accounKey1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0)
if err != nil {
t.Errorf("Error dering BIP44-compliant key")
}
if accounKey1.String() != derivedKey1String {
t.Errorf("BIP44Child: key mismatch -- got: %v, want: %v", accounKey1.String(), derivedKey1String)
}
t.Logf("Account 1 key: %s", accounKey1.String())
accounKey2, err := extKey.BIP44Child(extkeys.CoinTypeETH, 1)
if err != nil {
t.Errorf("Error dering BIP44-compliant key")
}
if accounKey2.String() != derivedKey2String {
t.Errorf("BIP44Child: key mismatch -- got: %v, want: %v", accounKey2.String(), derivedKey2String)
}
t.Logf("Account 1 key: %s", accounKey2.String())
}
//func TestNewKey(t *testing.T) {
// mnemonic := NewMnemonic()
//

View File

@ -14,9 +14,9 @@ var (
)
func splitHMAC(seed, salt []byte) (secretKey, chainCode []byte, err error) {
hmac := hmac.New(sha512.New, salt)
hmac.Write(seed)
I := hmac.Sum(nil)
data := hmac.New(sha512.New, salt)
data.Write(seed)
I := data.Sum(nil)
// Split I into two 32-byte sequences, IL and IR.
// IL = master secret key

View File

@ -9,13 +9,10 @@ extern bool GethServiceSignalEvent( const char *jsonEvent );
import "C"
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"time"
"encoding/json"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
@ -26,8 +23,13 @@ import (
)
var (
scryptN = 4096
scryptP = 6
ErrInvalidGethNode = errors.New("no running node detected for account unlock")
ErrInvalidWhisperService = errors.New("whisper service is unavailable")
ErrInvalidAccountManager = errors.New("could not retrieve account manager")
ErrAddressToAccountMappingFailure = errors.New("cannot retreive a valid account for a given address")
ErrAccountToKeyMappingFailure = errors.New("cannot retreive a valid key for a given account")
ErrUnlockCalled = errors.New("no need to unlock accounts, use Login() instead")
ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper")
)
// createAccount creates an internal geth account
@ -45,25 +47,27 @@ func createAccount(password string) (string, string, string, error) {
return "", "", "", errextra.Wrap(err, "Can not create mnemonic seed")
}
// generate extended key (see BIP32)
// generate extended master key (see BIP32)
extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
if err != nil {
return "", "", "", errextra.Wrap(err, "Can not create master extended key")
}
// derive hardened child (see BIP44)
extChild1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0)
if err != nil {
return "", "", "", errextra.Wrap(err, "Can not derive hardened child key (#1)")
}
// generate the account
account, err := accountManager.NewAccountUsingExtendedKey(extKey, password, w)
account, err := accountManager.NewAccountUsingExtendedKey(extChild1, password, w)
if err != nil {
return "", "", "", errextra.Wrap(err, "Account manager could not create the account")
}
address := fmt.Sprintf("%x", account.Address)
// recover the public key to return
keyContents, err := ioutil.ReadFile(account.File)
if err != nil {
return address, "", "", errextra.Wrap(err, "Could not load the key contents")
}
key, err := accounts.DecryptKey(keyContents, password)
account, key, err := accountManager.AccountDecryptedKey(account, password)
if err != nil {
return address, "", "", errextra.Wrap(err, "Could not recover the key")
}
@ -90,7 +94,13 @@ func remindAccountDetails(password, mnemonic string) (string, string, error) {
return "", "", errextra.Wrap(err, "Can not create master extended key")
}
privateKeyECDSA := extKey.ToECDSA()
// derive hardened child (see BIP44)
extChild1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0)
if err != nil {
return "", "", errextra.Wrap(err, "Can not derive hardened child key (#1)")
}
privateKeyECDSA := extChild1.ToECDSA()
address := fmt.Sprintf("%x", crypto.PubkeyToAddress(privateKeyECDSA.PublicKey))
pubKey := common.ToHex(crypto.FromECDSAPub(&privateKeyECDSA.PublicKey))
@ -105,30 +115,43 @@ func remindAccountDetails(password, mnemonic string) (string, string, error) {
return "", "", errors.New("No running node detected for account unlock")
}
func selectAccount(address, password string) error {
if currentNode == nil {
return ErrInvalidGethNode
}
if accountManager == nil {
return ErrInvalidAccountManager
}
account, err := utils.MakeAddress(accountManager, address)
if err != nil {
return ErrAddressToAccountMappingFailure
}
account, accountKey, err := accountManager.AccountDecryptedKey(account, password)
if err != nil {
return fmt.Errorf("%s: %v", ErrAccountToKeyMappingFailure.Error(), err)
}
if whisperService == nil {
return ErrInvalidWhisperService
}
if err := whisperService.InjectIdentity(accountKey.PrivateKey); err != nil {
return ErrWhisperIdentityInjectionFailure
}
return nil
}
// unlockAccount unlocks an existing account for a certain duration and
// inject the account as a whisper identity if the account was created as
// a whisper enabled account
func unlockAccount(address, password string, seconds int) error {
if currentNode != nil {
if accountManager != nil {
account, err := utils.MakeAddress(accountManager, address)
if err != nil {
return errextra.Wrap(err, "Could not retrieve account from address")
}
err = accountManager.TimedUnlock(account, password, time.Duration(seconds)*time.Second)
if err != nil {
return errextra.Wrap(err, "Could not decrypt account")
}
return nil
}
return errors.New("Could not retrieve account manager")
if currentNode == nil {
return ErrInvalidGethNode
}
return errors.New("No running node detected for account unlock")
return ErrUnlockCalled
}
// createAndStartNode creates a node entity and starts the
@ -172,12 +195,12 @@ func onSendTransactionRequest(queuedTx les.QueuedTx) {
C.GethServiceSignalEvent(C.CString(string(body)))
}
func completeTransaction(hash string) (common.Hash, error) {
func completeTransaction(hash, password string) (common.Hash, error) {
if currentNode != nil {
if lightEthereum != nil {
backend := lightEthereum.StatusBackend
return backend.CompleteQueuedTransaction(les.QueuedTxHash(hash))
return backend.CompleteQueuedTransaction(les.QueuedTxHash(hash), password)
}
return common.Hash{}, errors.New("can not retrieve LES service")

View File

@ -1,10 +1,10 @@
package main
import (
"fmt"
"os"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"testing"
"time"
@ -24,11 +24,16 @@ const (
testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4"
testAddressPassword = "asdf"
testNodeSyncSeconds = 180
testAccountPassword = "badpassword"
newAccountPassword = "badpassword"
whisperMessage1 = "test message 1 (K1 -> K1)"
whisperMessage2 = "test message 2 (K1 -> '')"
whisperMessage3 = "test message 3 ('' -> '')"
whisperMessage4 = "test message 4 ('' -> K1)"
whisperMessage5 = "test message 5 (K2 -> K1)"
)
// TestAccountBindings makes sure we can create an account and subsequently unlock that account
func TestAccountBindings(t *testing.T) {
func TestRemindAccountDetails(t *testing.T) {
err := prepareTestNode()
if err != nil {
t.Error(err)
@ -36,16 +41,15 @@ func TestAccountBindings(t *testing.T) {
}
// create an account
address, pubKey, mnemonic, err := createAccount(testAccountPassword)
address, pubKey, mnemonic, err := createAccount(newAccountPassword)
if err != nil {
fmt.Println(err.Error())
t.Error("Test failed: could not create account")
t.Errorf("could not create account: %v", err)
return
}
glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
// try reminding using password + mnemonic
addressCheck, pubKeyCheck, err := remindAccountDetails(testAccountPassword, mnemonic)
addressCheck, pubKeyCheck, err := remindAccountDetails(newAccountPassword, mnemonic)
if err != nil {
t.Errorf("remind details failed: %v", err)
return
@ -53,54 +57,241 @@ func TestAccountBindings(t *testing.T) {
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("Test failed: remind account details failed to pull the correct details")
}
}
// unlock the created account
err = unlockAccount(address, "badpassword", 3)
func TestAccountSelect(t *testing.T) {
err := prepareTestNode()
if err != nil {
fmt.Println(err)
t.Error("Test failed: could not unlock account")
t.Error(err)
return
}
time.Sleep(2 * time.Second)
// test to see if the account was injected in whisper
var whisperInstance *whisper.Whisper
if err := currentNode.Service(&whisperInstance); err != nil {
t.Errorf("whisper service not running: %v", err)
}
identitySucsess := whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey)))
if !identitySucsess || err != nil {
t.Errorf("Test failed: identity not injected into whisper: %v", err)
// create an accounts
address1, pubKey1, _, err := createAccount(newAccountPassword)
if err != nil {
fmt.Println(err.Error())
t.Error("Test failed: could not create account")
return
}
glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1)
address2, pubKey2, _, err := createAccount(newAccountPassword)
if err != nil {
fmt.Println(err.Error())
t.Error("Test failed: could not create account")
return
}
glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2)
// inject key of newly created account into Whisper, as identity
if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) {
t.Errorf("identity already present in whisper")
}
// test to see if we can post with the injected whisper identity
// try selecting with wrong password
err = selectAccount(address1, "wrongPassword")
if err == nil {
t.Errorf("select account is expected to throw error: wrong password used")
return
}
err = selectAccount(address1, newAccountPassword)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) {
t.Errorf("identity not injected into whisper: %v", err)
}
// select another account, make sure that previous account is wiped out from Whisper cache
if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) {
t.Errorf("identity already present in whisper")
}
err = selectAccount(address2, newAccountPassword)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
if !whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) {
t.Errorf("identity should be removed, but it is still present in whisper")
}
}
func TestWhisperMessaging(t *testing.T) {
err := prepareTestNode()
if err != nil {
t.Error(err)
return
}
// test to see if the account was injected in whisper
var whisperInstance *whisper.Whisper
if err := currentNode.Service(&whisperInstance); err != nil {
t.Errorf("whisper service not running: %v", err)
}
whisperAPI := whisper.NewPublicWhisperAPI(whisperInstance)
// prepare message
postArgs := whisper.PostArgs{
From: pubKey,
To: pubKey,
TTL: 100,
From: "",
To: "",
TTL: 10,
Topics: [][]byte{[]byte("test topic")},
Payload: "test message",
}
whisperAPI := whisper.NewPublicWhisperAPI(whisperInstance)
postSuccess, err := whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("Test failed: Could not post to whisper: %v", err)
}
// import test account (with test ether on it)
err = copyFile(filepath.Join(testDataDir, "testnet", "keystore", "test-account.pk"), filepath.Join("data", "test-account.pk"))
// create an accounts
address1, pubKey1, _, err := createAccount(newAccountPassword)
if err != nil {
t.Errorf("Test failed: cannot copy test account PK: %v", err)
fmt.Println(err.Error())
t.Error("Test failed: could not create account")
return
}
time.Sleep(2 * time.Second)
glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1)
// unlock test account (to send ether from it)
err = unlockAccount(testAddress, testAddressPassword, 300)
address2, pubKey2, _, err := createAccount(newAccountPassword)
if err != nil {
fmt.Println(err)
t.Error("Test failed: could not unlock account")
fmt.Println(err.Error())
t.Error("Test failed: could not create account")
return
}
time.Sleep(2 * time.Second)
glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2)
// start watchers
var receivedMessages = map[string]bool{
whisperMessage1: false,
whisperMessage2: false,
whisperMessage3: false,
whisperMessage4: false,
whisperMessage5: false,
}
whisperService.Watch(whisper.Filter{
//From: crypto.ToECDSAPub(common.FromHex(pubKey1)),
//To: crypto.ToECDSAPub(common.FromHex(pubKey2)),
Fn: func(msg *whisper.Message) {
glog.V(logger.Info).Infof("Whisper message received: %s", msg.Payload)
receivedMessages[string(msg.Payload)] = true
},
})
// inject key of newly created account into Whisper, as identity
if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) {
t.Errorf("identity already present in whisper")
}
err = selectAccount(address1, newAccountPassword)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
identitySucceess := whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1)))
if !identitySucceess || err != nil {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { // ensure that second id is not injected
t.Errorf("identity already present in whisper")
}
// double selecting (shouldn't be a problem)
err = selectAccount(address1, newAccountPassword)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
// TEST 0: From != nil && To != nil: encrypted signed message (but we cannot decrypt it - so watchers will not report this)
postArgs.From = pubKey1
postArgs.To = pubKey2 // owner of that public key will be able to decrypt it
postSuccess, err := whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("could not post to whisper: %v", err)
}
// TEST 1: From != nil && To != nil: encrypted signed message (to self)
postArgs.From = pubKey1
postArgs.To = pubKey1
postArgs.Payload = whisperMessage1
postSuccess, err = whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("could not post to whisper: %v", err)
}
// send from account that is not in Whisper identity list
postArgs.From = pubKey2
postSuccess, err = whisperAPI.Post(postArgs)
if err == nil || err.Error() != fmt.Sprintf("unknown identity to send from: %s", pubKey2) {
t.Errorf("expected error not voiced: we are sending from non-injected whisper identity")
}
// TEST 2: From != nil && To == nil: signed broadcast (known sender)
postArgs.From = pubKey1
postArgs.To = ""
postArgs.Payload = whisperMessage2
postSuccess, err = whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("could not post to whisper: %v", err)
}
// TEST 3: From == nil && To == nil: anonymous broadcast
postArgs.From = ""
postArgs.To = ""
postArgs.Payload = whisperMessage3
postSuccess, err = whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("could not post to whisper: %v", err)
}
// TEST 4: From == nil && To != nil: encrypted anonymous message
postArgs.From = ""
postArgs.To = pubKey1
postArgs.Payload = whisperMessage4
postSuccess, err = whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("could not post to whisper: %v", err)
}
// TEST 5: From != nil && To != nil: encrypted and signed response
postArgs.From = ""
postArgs.To = pubKey1
postArgs.Payload = whisperMessage5
postSuccess, err = whisperAPI.Post(postArgs)
if !postSuccess || err != nil {
t.Errorf("could not post to whisper: %v", err)
}
time.Sleep(2 * time.Second) // allow whisper to poll
for message, status := range receivedMessages {
if !status {
t.Errorf("Expected message not received: %s", message)
}
}
}
func TestQueuedTransactions(t *testing.T) {
err := prepareTestNode()
if err != nil {
t.Error(err)
return
}
// create an account
address, pubKey, mnemonic, err := createAccount(newAccountPassword)
if err != nil {
fmt.Println(err.Error())
t.Error("Test failed: could not create account")
return
}
glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
// test transaction queueing
var lightEthereum *les.LightEthereum
@ -114,8 +305,9 @@ func TestAccountBindings(t *testing.T) {
backend.SetTransactionQueueHandler(func(queuedTx les.QueuedTx) {
glog.V(logger.Info).Infof("Queued transaction hash: %v\n", queuedTx.Hash.Hex())
var txHash common.Hash
if txHash, err = completeTransaction(queuedTx.Hash.Hex()); err != nil {
if txHash, err = completeTransaction(queuedTx.Hash.Hex(), testAddressPassword); err != nil {
t.Errorf("Test failed: cannot complete queued transation[%s]: %v", queuedTx.Hash.Hex(), err)
return
}
glog.V(logger.Info).Infof("Transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex())
@ -123,7 +315,7 @@ func TestAccountBindings(t *testing.T) {
})
// try completing non-existing transaction
if _, err := completeTransaction("0x1234512345123451234512345123456123451234512345123451234512345123"); err == nil {
if _, err := completeTransaction("0x1234512345123451234512345123456123451234512345123451234512345123", testAddressPassword); err == nil {
t.Errorf("Test failed: error expected and not recieved")
}
@ -152,14 +344,13 @@ func TestAccountBindings(t *testing.T) {
t.Error("Test failed: transaction was never queued or completed")
}
//// clean up
//err = os.RemoveAll(".ethereumtest")
//if err != nil {
// t.Error("Test failed: could not clean up temporary datadir")
//}
}
func prepareTestNode() error {
if currentNode != nil {
return nil
}
rpcport = 8546 // in order to avoid conflicts with running react-native app
syncRequired := false
@ -173,6 +364,13 @@ func prepareTestNode() error {
return err
}
// import test account (with test ether on it)
err = copyFile(filepath.Join(testDataDir, "testnet", "keystore", "test-account.pk"), filepath.Join("data", "test-account.pk"))
if err != nil {
glog.V(logger.Warn).Infof("Test failed: cannot copy test account PK: %v", err)
return err
}
// start geth node and wait for it to initialize
go createAndStartNode(dataDir)
time.Sleep(5 * time.Second)
@ -183,7 +381,16 @@ func prepareTestNode() error {
if syncRequired {
glog.V(logger.Warn).Infof("Sync is required, it will take %d seconds", testNodeSyncSeconds)
time.Sleep(testNodeSyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done
} else {
time.Sleep(10 * time.Second)
}
return nil
}
func cleanup() {
err := os.RemoveAll(testDataDir)
if err != nil {
glog.V(logger.Warn).Infof("Test failed: could not clean up temporary datadir")
}
}

View File

@ -58,10 +58,22 @@ func RemindAccountDetails(password, mnemonic *C.char) *C.char {
//export Login
func Login(address, password *C.char) *C.char {
// Equivalent to unlocking an account briefly, to inject a whisper identity,
// then locking the account again
out := UnlockAccount(address, password, 1)
return out
// loads a key file (for a given address), tries to decrypt it using the password, to verify ownership
// if verified, purges all the previous identities from Whisper, and injects verified key as shh identity
err := selectAccount(C.GoString(address), C.GoString(password))
errString := emptyError
if err != nil {
fmt.Fprintln(os.Stderr, err)
errString = err.Error()
}
out := JSONError{
Error: errString,
}
outBytes, _ := json.Marshal(&out)
return C.CString(string(outBytes))
}
//export UnlockAccount
@ -87,8 +99,8 @@ func UnlockAccount(address, password *C.char, seconds int) *C.char {
}
//export CompleteTransaction
func CompleteTransaction(hash *C.char) *C.char {
txHash, err := completeTransaction(C.GoString(hash))
func CompleteTransaction(hash, password *C.char) *C.char {
txHash, err := completeTransaction(C.GoString(hash), C.GoString(password))
errString := emptyError
if err != nil {

View File

@ -137,7 +137,7 @@ func makeDefaultExtra() []byte {
}
func preprocessDataDir(dataDir string) (string, error) {
testDataDir := path.Join(dataDir, "testnet")
testDataDir := path.Join(dataDir, "testnet", "keystore")
if _, err := os.Stat(testDataDir); os.IsNotExist(err) {
if err := os.MkdirAll(testDataDir, 0755); err != nil {
return dataDir, ErrDataDirPreprocessingFailed
@ -145,7 +145,7 @@ func preprocessDataDir(dataDir string) (string, error) {
}
// copy over static peer nodes list (LES auto-discovery is not stable yet)
dst := filepath.Join(testDataDir, "static-nodes.json")
dst := filepath.Join(dataDir, "testnet", "static-nodes.json")
if _, err := os.Stat(dst); os.IsNotExist(err) {
src := filepath.Join("data", "static-nodes.json")
if err := copyFile(dst, src); err != nil {

View File

@ -123,6 +123,10 @@ func (am *Manager) Accounts() []Account {
return am.cache.accounts()
}
func (am *Manager) AccountDecryptedKey(a Account, auth string) (Account, *Key, error) {
return am.getDecryptedKey(a, auth)
}
// DeleteAccount deletes the key matched by account if the passphrase is correct.
// If a contains no filename, the address must match a unique key.
func (am *Manager) DeleteAccount(a Account, passphrase string) error {
@ -327,15 +331,6 @@ func (am *Manager) NewAccountUsingExtendedKey(k *extkeys.ExtendedKey, passphrase
// than waiting for file system notifications to pick it up.
am.cache.add(account)
// sync key to subprotocols (e.g., whisper identity)
if am.sync != nil {
address := fmt.Sprintf("%x", account.Address)
err := am.syncAccounts(address, key)
if err != nil {
return account, fmt.Errorf("failed to sync accounts: %s", err.Error())
}
}
return account, nil
}

View File

@ -1173,7 +1173,7 @@ func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args Sen
// CompleteQueuedTransaction creates a transaction by unpacking queued transaction, signs it and submits to the
// transaction pool.
func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) {
var err error
args, err = prepareSendTxArgs(ctx, args, s.b)
if err != nil {
@ -1195,7 +1195,7 @@ func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context
tx = types.NewTransaction(args.Nonce.Uint64(), *args.To, args.Value.BigInt(), args.Gas.BigInt(), args.GasPrice.BigInt(), common.FromHex(args.Data))
}
signature, err := s.b.AccountManager().Sign(args.From, tx.SigHash().Bytes())
signature, err := s.b.AccountManager().SignWithPassphrase(args.From, passphrase, tx.SigHash().Bytes())
if err != nil {
return common.Hash{}, err
}

View File

@ -102,13 +102,13 @@ func (b *StatusBackend) SendTransaction(ctx context.Context, args SendTxArgs) er
}
// CompleteQueuedTransaction wraps call to PublicTransactionPoolAPI.CompleteQueuedTransaction
func (b *StatusBackend) CompleteQueuedTransaction(hash QueuedTxHash) (common.Hash, error) {
func (b *StatusBackend) CompleteQueuedTransaction(hash QueuedTxHash, passphrase string) (common.Hash, error) {
queuedTx, err := b.txEvictingQueue.getQueuedTransaction(hash)
if err != nil {
return common.Hash{}, err
}
return b.txapi.CompleteQueuedTransaction(context.Background(), ethapi.SendTxArgs(queuedTx.Args))
return b.txapi.CompleteQueuedTransaction(context.Background(), ethapi.SendTxArgs(queuedTx.Args), passphrase)
}
// GetTransactionQueue wraps call to PublicTransactionPoolAPI.GetTransactionQueue

View File

@ -65,7 +65,8 @@ type Whisper struct {
protocol p2p.Protocol
filters *filter.Filters
keys map[string]*ecdsa.PrivateKey
keys map[string]*ecdsa.PrivateKey
keysMu sync.RWMutex // Mutex to sync identity keys
messages map[common.Hash]*Envelope // Pool of messages currently tracked by this node
expirations map[uint32]*set.SetNonTS // Message expiration pool (TODO: something lighter)
@ -130,7 +131,9 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey {
if err != nil {
panic(err)
}
self.keysMu.Lock()
self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key
self.keysMu.Unlock()
return key
}
@ -138,25 +141,32 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey {
// HasIdentity checks if the the whisper node is configured with the private key
// of the specified public pair.
func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool {
self.keysMu.RLock()
defer self.keysMu.RUnlock()
return self.keys[string(crypto.FromECDSAPub(key))] != nil
}
// GetIdentity retrieves the private key of the specified public identity.
func (self *Whisper) GetIdentity(key *ecdsa.PublicKey) *ecdsa.PrivateKey {
self.keysMu.RLock()
defer self.keysMu.RUnlock()
return self.keys[string(crypto.FromECDSAPub(key))]
}
// InjectIdentity injects a manually added identity/key pair into the whisper keys
func (self *Whisper) InjectIdentity(key *ecdsa.PrivateKey) error {
identity := string(crypto.FromECDSAPub(&key.PublicKey))
self.keys[identity] = key
if _, ok := self.keys[identity]; !ok {
return fmt.Errorf("key insert into keys map failed")
if self.HasIdentity(&key.PublicKey) { // no need to re-inject
return nil
}
identityString := common.ToHex(crypto.FromECDSAPub(&key.PublicKey))
fmt.Printf("Injected identity into whisper: %s\n", identityString)
self.keysMu.Lock()
self.keys = make(map[string]*ecdsa.PrivateKey) // reset key store
self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key
self.keysMu.Unlock()
fmt.Printf("Injected identity into whisper: %s\n", common.ToHex(crypto.FromECDSAPub(&key.PublicKey)))
return nil
}
@ -308,6 +318,9 @@ func (self *Whisper) postEvent(envelope *Envelope) {
// returning the decrypted message and the key used to achieve it. If not keys
// are configured, open will return the payload as if non encrypted.
func (self *Whisper) open(envelope *Envelope) *Message {
self.keysMu.RLock()
defer self.keysMu.RUnlock()
// Short circuit if no identity is set, and assume clear-text
if len(self.keys) == 0 {
if message, err := envelope.Open(nil); err == nil {