parent
3f6133e44b
commit
6ad3f8786e
9 changed files with 398 additions and 51 deletions
|
@ -130,7 +130,7 @@ func (a *Account) Lock() error {
|
|||
}
|
||||
|
||||
// Unlock unlocks the underlying wallet for an account.
|
||||
func (a *Account) Unlock(passphrase []byte, timeout int64) error {
|
||||
func (a *Account) Unlock(passphrase []byte) error {
|
||||
a.mtx.Lock()
|
||||
defer a.mtx.Unlock()
|
||||
|
||||
|
|
|
@ -226,6 +226,75 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [
|
|||
return nil
|
||||
}
|
||||
|
||||
// ChangePassphrase unlocks all account wallets with the old
|
||||
// passphrase, and re-encrypts each using the new passphrase.
|
||||
func (store *AccountStore) ChangePassphrase(old, new []byte) error {
|
||||
store.RLock()
|
||||
defer store.RUnlock()
|
||||
|
||||
// Check that each account can be unlocked with the old passphrase.
|
||||
// Each's account's wallet mutex is unlocked with a defer so they
|
||||
// will be held for the duration of this function. This prevents
|
||||
// a wallet from being locked after some timeout after a RPC call
|
||||
// to walletpassphrase.
|
||||
for _, a := range store.accounts {
|
||||
a.mtx.Lock()
|
||||
defer a.mtx.Unlock()
|
||||
|
||||
if locked := a.Wallet.IsLocked(); !locked {
|
||||
if err := a.Wallet.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.Wallet.Unlock(old); err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.Wallet.Lock()
|
||||
}
|
||||
|
||||
// Change passphrase for each unlocked wallet.
|
||||
for _, a := range store.accounts {
|
||||
if err := a.Wallet.ChangePassphrase(new); err != nil {
|
||||
return err
|
||||
}
|
||||
a.dirty = true
|
||||
}
|
||||
|
||||
// Immediately write out to disk. Create a new temporary network
|
||||
// directory to write to, write all account files there, then move
|
||||
// to the real network directory. This provides an safe
|
||||
// replacement of all account files and ensures that all wallets
|
||||
// are using either the old or new passphrase, but never two wallets
|
||||
// with different passphrases.
|
||||
netDir := networkDir(cfg.Net())
|
||||
tmpNetDir := tmpNetworkDir(cfg.Net())
|
||||
for _, a := range store.accounts {
|
||||
// Writer locks must be held for the tx and utxo stores as well,
|
||||
// to unset the dirty flag.
|
||||
a.UtxoStore.Lock()
|
||||
defer a.UtxoStore.Unlock()
|
||||
a.TxStore.Lock()
|
||||
defer a.TxStore.Unlock()
|
||||
|
||||
if err := a.writeAllToFreshDir(tmpNetDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// This is technically NOT an atomic operation, but at startup, if the
|
||||
// network directory is missing but the temporary network directory
|
||||
// exists, the temporary is moved before accounts are opened.
|
||||
if err := os.RemoveAll(netDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := Rename(tmpNetDir, netDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DumpKeys returns all WIF-encoded private keys associated with all
|
||||
// accounts. All wallets must be unlocked for this operation to succeed.
|
||||
func (store *AccountStore) DumpKeys() ([]string, error) {
|
||||
|
|
17
cmd.go
17
cmd.go
|
@ -248,6 +248,21 @@ func main() {
|
|||
|
||||
// OpenAccounts attempts to open all saved accounts.
|
||||
func OpenAccounts() {
|
||||
// If the network (account) directory is missing, but the temporary
|
||||
// directory exists, move it. This is unlikely to happen, but possible,
|
||||
// if writing out every account file at once to a tmp directory (as is
|
||||
// done for changing a wallet passphrase) and btcwallet closes after
|
||||
// removing the network directory but before renaming the temporary
|
||||
// directory.
|
||||
netDir := networkDir(cfg.Net())
|
||||
tmpNetDir := tmpNetworkDir(cfg.Net())
|
||||
if !fileExists(netDir) && fileExists(tmpNetDir) {
|
||||
if err := Rename(tmpNetDir, netDir); err != nil {
|
||||
log.Errorf("Cannot move temporary network dir: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The default account must exist, or btcwallet acts as if no
|
||||
// wallets/accounts have been created yet.
|
||||
if err := accountstore.OpenAccount("", cfg); err != nil {
|
||||
|
@ -264,7 +279,7 @@ func OpenAccounts() {
|
|||
// Read all filenames in the account directory, and look for any
|
||||
// filenames matching '*-wallet.bin'. These are wallets for
|
||||
// additional saved accounts.
|
||||
accountDir, err := os.Open(networkDir(cfg.Net()))
|
||||
accountDir, err := os.Open(netDir)
|
||||
if err != nil {
|
||||
// Can't continue.
|
||||
log.Errorf("Unable to open account directory: %v", err)
|
||||
|
|
119
cmdmgr.go
119
cmdmgr.go
|
@ -32,51 +32,51 @@ type cmdHandler func(btcjson.Cmd) (interface{}, *btcjson.Error)
|
|||
|
||||
var rpcHandlers = map[string]cmdHandler{
|
||||
// Standard bitcoind methods (implemented)
|
||||
"dumpprivkey": DumpPrivKey,
|
||||
"getaccount": GetAccount,
|
||||
"getaccountaddress": GetAccountAddress,
|
||||
"getaddressesbyaccount": GetAddressesByAccount,
|
||||
"getbalance": GetBalance,
|
||||
"getnewaddress": GetNewAddress,
|
||||
"importprivkey": ImportPrivKey,
|
||||
"keypoolrefill": KeypoolRefill,
|
||||
"listaccounts": ListAccounts,
|
||||
"listtransactions": ListTransactions,
|
||||
"sendfrom": SendFrom,
|
||||
"sendmany": SendMany,
|
||||
"settxfee": SetTxFee,
|
||||
"walletlock": WalletLock,
|
||||
"walletpassphrase": WalletPassphrase,
|
||||
"dumpprivkey": DumpPrivKey,
|
||||
"getaccount": GetAccount,
|
||||
"getaccountaddress": GetAccountAddress,
|
||||
"getaddressesbyaccount": GetAddressesByAccount,
|
||||
"getbalance": GetBalance,
|
||||
"getnewaddress": GetNewAddress,
|
||||
"importprivkey": ImportPrivKey,
|
||||
"keypoolrefill": KeypoolRefill,
|
||||
"listaccounts": ListAccounts,
|
||||
"listtransactions": ListTransactions,
|
||||
"sendfrom": SendFrom,
|
||||
"sendmany": SendMany,
|
||||
"settxfee": SetTxFee,
|
||||
"walletlock": WalletLock,
|
||||
"walletpassphrase": WalletPassphrase,
|
||||
"walletpassphrasechange": WalletPassphraseChange,
|
||||
|
||||
// Standard bitcoind methods (currently unimplemented)
|
||||
"addmultisigaddress": Unimplemented,
|
||||
"backupwallet": Unimplemented,
|
||||
"createmultisig": Unimplemented,
|
||||
"dumpwallet": Unimplemented,
|
||||
"getblocktemplate": Unimplemented,
|
||||
"getrawchangeaddress": Unimplemented,
|
||||
"getreceivedbyaccount": Unimplemented,
|
||||
"getreceivedbyaddress": Unimplemented,
|
||||
"gettransaction": Unimplemented,
|
||||
"gettxout": Unimplemented,
|
||||
"gettxoutsetinfo": Unimplemented,
|
||||
"getwork": Unimplemented,
|
||||
"importwallet": Unimplemented,
|
||||
"listaddressgroupings": Unimplemented,
|
||||
"listlockunspent": Unimplemented,
|
||||
"listreceivedbyaccount": Unimplemented,
|
||||
"listsinceblock": Unimplemented,
|
||||
"listreceivedbyaddress": Unimplemented,
|
||||
"listunspent": Unimplemented,
|
||||
"lockunspent": Unimplemented,
|
||||
"move": Unimplemented,
|
||||
"sendtoaddress": Unimplemented,
|
||||
"setaccount": Unimplemented,
|
||||
"signmessage": Unimplemented,
|
||||
"signrawtransaction": Unimplemented,
|
||||
"validateaddress": Unimplemented,
|
||||
"verifymessage": Unimplemented,
|
||||
"walletpassphrasechange": Unimplemented,
|
||||
"addmultisigaddress": Unimplemented,
|
||||
"backupwallet": Unimplemented,
|
||||
"createmultisig": Unimplemented,
|
||||
"dumpwallet": Unimplemented,
|
||||
"getblocktemplate": Unimplemented,
|
||||
"getrawchangeaddress": Unimplemented,
|
||||
"getreceivedbyaccount": Unimplemented,
|
||||
"getreceivedbyaddress": Unimplemented,
|
||||
"gettransaction": Unimplemented,
|
||||
"gettxout": Unimplemented,
|
||||
"gettxoutsetinfo": Unimplemented,
|
||||
"getwork": Unimplemented,
|
||||
"importwallet": Unimplemented,
|
||||
"listaddressgroupings": Unimplemented,
|
||||
"listlockunspent": Unimplemented,
|
||||
"listreceivedbyaccount": Unimplemented,
|
||||
"listsinceblock": Unimplemented,
|
||||
"listreceivedbyaddress": Unimplemented,
|
||||
"listunspent": Unimplemented,
|
||||
"lockunspent": Unimplemented,
|
||||
"move": Unimplemented,
|
||||
"sendtoaddress": Unimplemented,
|
||||
"setaccount": Unimplemented,
|
||||
"signmessage": Unimplemented,
|
||||
"signrawtransaction": Unimplemented,
|
||||
"validateaddress": Unimplemented,
|
||||
"verifymessage": Unimplemented,
|
||||
|
||||
// Standard bitcoind methods which won't be implemented by btcwallet.
|
||||
"encryptwallet": Unsupported,
|
||||
|
@ -1277,7 +1277,7 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
|||
return nil, &e
|
||||
}
|
||||
|
||||
switch err := a.Unlock([]byte(cmd.Passphrase), cmd.Timeout); err {
|
||||
switch err := a.Unlock([]byte(cmd.Passphrase)); err {
|
||||
case nil:
|
||||
go func(timeout int64) {
|
||||
time.Sleep(time.Second * time.Duration(timeout))
|
||||
|
@ -1293,6 +1293,37 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
|||
}
|
||||
}
|
||||
|
||||
// WalletPassphraseChange responds to the walletpassphrasechange request
|
||||
// by unlocking all accounts with the provided old passphrase, and
|
||||
// re-encrypting each private key with an AES key derived from the new
|
||||
// passphrase.
|
||||
//
|
||||
// If the old passphrase is correct and the passphrase is changed, all
|
||||
// wallets will be immediately locked.
|
||||
func WalletPassphraseChange(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
||||
cmd, ok := icmd.(*btcjson.WalletPassphraseChangeCmd)
|
||||
if !ok {
|
||||
return nil, &btcjson.ErrInternal
|
||||
}
|
||||
|
||||
err := accountstore.ChangePassphrase([]byte(cmd.OldPassphrase),
|
||||
[]byte(cmd.NewPassphrase))
|
||||
switch err {
|
||||
case nil:
|
||||
return nil, nil
|
||||
|
||||
case wallet.ErrWrongPassphrase:
|
||||
return nil, &btcjson.ErrWalletPassphraseIncorrect
|
||||
|
||||
default: // all other non-nil errors
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
}
|
||||
|
||||
// AccountNtfn is a struct for marshalling any generic notification
|
||||
// about a account for a wallet frontend.
|
||||
//
|
||||
|
|
85
disksync.go
85
disksync.go
|
@ -36,8 +36,8 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// networkDir returns the base directory name for the bitcoin network
|
||||
// net.
|
||||
// networkDir returns the directory name of a network directory to hold account
|
||||
// files.
|
||||
func networkDir(net btcwire.BitcoinNet) string {
|
||||
var netname string
|
||||
if net == btcwire.MainNet {
|
||||
|
@ -48,6 +48,11 @@ func networkDir(net btcwire.BitcoinNet) string {
|
|||
return filepath.Join(cfg.DataDir, netname)
|
||||
}
|
||||
|
||||
// tmpNetworkDir returns the temporary directory name for a given network.
|
||||
func tmpNetworkDir(net btcwire.BitcoinNet) string {
|
||||
return networkDir(net) + "_tmp"
|
||||
}
|
||||
|
||||
// checkCreateDir checks that the path exists and is a directory.
|
||||
// If path does not exist, it is created.
|
||||
func checkCreateDir(path string) error {
|
||||
|
@ -109,6 +114,82 @@ func DirtyAccountSyncer() {
|
|||
}
|
||||
}
|
||||
|
||||
// freshDir creates a new directory specified by path if it does not
|
||||
// exist. If the directory already exists, all files contained in the
|
||||
// directory are removed.
|
||||
func freshDir(path string) error {
|
||||
if err := checkCreateDir(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all files in the directory.
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
names, err := fd.Readdirnames(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range names {
|
||||
if err := os.RemoveAll(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeAllToFreshDir writes all account files to the specified directory.
|
||||
// If dir already exists, any old files are removed. If dir does not
|
||||
// exist, it is created.
|
||||
//
|
||||
// It is a runtime error to call this function while not holding each
|
||||
// wallet, tx store, and utxo store writer lock.
|
||||
func (a *Account) writeAllToFreshDir(dir string) error {
|
||||
if err := freshDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wfilepath := accountFilename("wallet.bin", a.name, dir)
|
||||
txfilepath := accountFilename("tx.bin", a.name, dir)
|
||||
utxofilepath := accountFilename("utxo.bin", a.name, dir)
|
||||
|
||||
wfile, err := os.Create(wfilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wfile.Close()
|
||||
txfile, err := os.Create(txfilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer txfile.Close()
|
||||
utxofile, err := os.Create(utxofilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utxofile.Close()
|
||||
|
||||
if _, err := a.Wallet.WriteTo(wfile); err != nil {
|
||||
return err
|
||||
}
|
||||
a.dirty = false
|
||||
|
||||
if _, err := a.TxStore.s.WriteTo(txfile); err != nil {
|
||||
return err
|
||||
}
|
||||
a.TxStore.dirty = false
|
||||
|
||||
if _, err := a.UtxoStore.s.WriteTo(utxofile); err != nil {
|
||||
return err
|
||||
}
|
||||
a.UtxoStore.dirty = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeDirtyToDisk checks for the dirty flag on an account's wallet,
|
||||
// txstore, and utxostore, writing them to disk if any are dirty.
|
||||
func (a *Account) writeDirtyToDisk() error {
|
||||
|
|
1
tx/tx.go
1
tx/tx.go
|
@ -109,6 +109,7 @@ type Tx interface {
|
|||
ReadFromVersion(uint32, io.Reader) (int64, error)
|
||||
TxInfo(string, int32, btcwire.BitcoinNet) []map[string]interface{}
|
||||
}
|
||||
|
||||
// TxStore is a slice holding RecvTx and SendTx pointers.
|
||||
type TxStore []Tx
|
||||
|
||||
|
|
16
updates.go
16
updates.go
|
@ -75,6 +75,22 @@ func updateOldFileLocations() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
acctsExist := false
|
||||
for i := range fi {
|
||||
// Ignore non-directories.
|
||||
if !fi[i].IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(fi[i].Name(), "btcwallet") {
|
||||
acctsExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !acctsExist {
|
||||
return
|
||||
}
|
||||
|
||||
// Create testnet directory, if it doesn't already exist.
|
||||
netdir := filepath.Join(cfg.DataDir, "testnet")
|
||||
if err := checkCreateDir(netdir); err != nil {
|
||||
|
|
|
@ -61,6 +61,7 @@ var (
|
|||
ErrWalletDoesNotExist = errors.New("non-existant wallet")
|
||||
ErrWalletIsWatchingOnly = errors.New("wallet is watching-only")
|
||||
ErrWalletLocked = errors.New("wallet is locked")
|
||||
ErrWrongPassphrase = errors.New("wrong passphrase")
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -845,6 +846,37 @@ func (w *Wallet) Passphrase() ([]byte, error) {
|
|||
return nil, ErrWalletLocked
|
||||
}
|
||||
|
||||
// ChangePassphrase creates a new AES key from a new passphrase and
|
||||
// re-encrypts all encrypted private keys with the new key.
|
||||
func (w *Wallet) ChangePassphrase(new []byte) error {
|
||||
if w.flags.watchingOnly {
|
||||
return ErrWalletIsWatchingOnly
|
||||
}
|
||||
|
||||
if len(w.secret) != 32 {
|
||||
return ErrWalletLocked
|
||||
}
|
||||
|
||||
oldkey := w.secret
|
||||
newkey := Key(new, &w.kdfParams)
|
||||
|
||||
for _, a := range w.addrMap {
|
||||
if err := a.changeEncryptionKey(oldkey, newkey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// zero old secrets.
|
||||
zero(w.passphrase)
|
||||
zero(w.secret)
|
||||
|
||||
// Save new secrets.
|
||||
w.passphrase = new
|
||||
w.secret = newkey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func zero(b []byte) {
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
|
@ -2151,7 +2183,7 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
|
|||
}
|
||||
x, y := btcec.S256().ScalarBaseMult(privkey)
|
||||
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
|
||||
return nil, errors.New("decryption failed")
|
||||
return nil, ErrWrongPassphrase
|
||||
}
|
||||
|
||||
privkeyCopy := make([]byte, 32)
|
||||
|
@ -2160,9 +2192,36 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
|
|||
return privkeyCopy, nil
|
||||
}
|
||||
|
||||
// TODO(jrick)
|
||||
// changeEncryptionKey re-encrypts the private keys for an address
|
||||
// with a new AES encryption key. oldkey must be the old AES encryption key
|
||||
// and is used to decrypt the private key.
|
||||
func (a *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
|
||||
return errors.New("unimplemented")
|
||||
// Address must have a private key and be encrypted to continue.
|
||||
if !a.flags.hasPrivKey {
|
||||
return errors.New("no private key")
|
||||
}
|
||||
if !a.flags.encrypted {
|
||||
return errors.New("address is not encrypted")
|
||||
}
|
||||
|
||||
privKeyCT, err := a.unlock(oldkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aesBlockEncrypter, err := aes.NewCipher(newkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newIV := make([]byte, len(a.initVector))
|
||||
if _, err := rand.Read(newIV); err != nil {
|
||||
return err
|
||||
}
|
||||
copy(a.initVector[:], newIV)
|
||||
aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:])
|
||||
aesEncrypter.XORKeyStream(a.privKey[:], privKeyCT)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// address returns a btcutil.AddressPubKeyHash for a btcAddress.
|
||||
|
|
|
@ -667,3 +667,78 @@ func TestWatchingWalletExport(t *testing.T) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePassphrase(t *testing.T) {
|
||||
const keypoolSize = 10
|
||||
createdAt := &BlockStamp{}
|
||||
w, err := NewWallet("banana wallet", "A wallet for testing.",
|
||||
[]byte("banana"), btcwire.MainNet, createdAt, keypoolSize)
|
||||
if err != nil {
|
||||
t.Error("Error creating new wallet: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Changing the passphrase with a locked wallet must fail with ErrWalletLocked.
|
||||
if err := w.ChangePassphrase([]byte("potato")); err != ErrWalletLocked {
|
||||
t.Errorf("Changing passphrase on a locked wallet did not fail correctly: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock wallet so the passphrase can be changed.
|
||||
if err := w.Unlock([]byte("banana")); err != nil {
|
||||
t.Errorf("Cannot unlock: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get root address and its private key. This is compared to the private
|
||||
// key post passphrase change.
|
||||
rootAddr := w.LastChainedAddress()
|
||||
rootPrivKey, err := w.AddressKey(rootAddr)
|
||||
if err != nil {
|
||||
t.Errorf("Cannot get root address' private key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Change passphrase.
|
||||
if err := w.ChangePassphrase([]byte("potato")); err != nil {
|
||||
t.Errorf("Changing passhprase failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Wallet should still be unlocked.
|
||||
if w.IsLocked() {
|
||||
t.Errorf("Wallet should be unlocked after passphrase change.")
|
||||
return
|
||||
}
|
||||
|
||||
// Lock it.
|
||||
if err := w.Lock(); err != nil {
|
||||
t.Errorf("Cannot lock wallet after passphrase change: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock with old passphrase. This must fail with ErrWrongPassphrase.
|
||||
if err := w.Unlock([]byte("banana")); err != ErrWrongPassphrase {
|
||||
t.Errorf("Unlocking with old passphrases did not fail correctly: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock with new passphrase. This must succeed.
|
||||
if err := w.Unlock([]byte("potato")); err != nil {
|
||||
t.Errorf("Unlocking with new passphrase failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get root address' private key again.
|
||||
rootPrivKey2, err := w.AddressKey(rootAddr)
|
||||
if err != nil {
|
||||
t.Errorf("Cannot get root address' private key after passphrase change: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Private keys must match.
|
||||
if !reflect.DeepEqual(rootPrivKey, rootPrivKey2) {
|
||||
t.Errorf("Private keys before and after unlock differ.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue