Merge pull request #535 from cfromknecht/notify-addrs-after-db-commit

wallet/wallet: notify addrs+props after db commit
This commit is contained in:
Olaoluwa Osuntokun 2018-09-03 17:42:28 -07:00 committed by GitHub
commit d14d889e88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 139 additions and 75 deletions

View file

@ -21,20 +21,6 @@ import (
// for change when the tests are run. // for change when the tests are run.
var TstLatestMgrVersion = &latestMgrVersion var TstLatestMgrVersion = &latestMgrVersion
// Replace the Manager.newSecretKey function with the given one and calls
// the callback function. Afterwards the original newSecretKey
// function will be restored.
func TstRunWithReplacedNewSecretKey(callback func()) {
orig := newSecretKey
defer func() {
newSecretKey = orig
}()
newSecretKey = func(passphrase *[]byte, config *ScryptOptions) (*snacl.SecretKey, error) {
return nil, snacl.ErrDecryptFailed
}
callback()
}
// TstCheckPublicPassphrase returns true if the provided public passphrase is // TstCheckPublicPassphrase returns true if the provided public passphrase is
// correct for the manager. // correct for the manager.
func (m *Manager) TstCheckPublicPassphrase(pubPassphrase []byte) bool { func (m *Manager) TstCheckPublicPassphrase(pubPassphrase []byte) bool {

View file

@ -174,15 +174,46 @@ type unlockDeriveInfo struct {
index uint32 index uint32
} }
// SecretKeyGenerator is the function signature of a method that can generate
// secret keys for the address manager.
type SecretKeyGenerator func(
passphrase *[]byte, config *ScryptOptions) (*snacl.SecretKey, error)
// defaultNewSecretKey returns a new secret key. See newSecretKey. // defaultNewSecretKey returns a new secret key. See newSecretKey.
func defaultNewSecretKey(passphrase *[]byte, config *ScryptOptions) (*snacl.SecretKey, error) { func defaultNewSecretKey(passphrase *[]byte,
config *ScryptOptions) (*snacl.SecretKey, error) {
return snacl.NewSecretKey(passphrase, config.N, config.R, config.P) return snacl.NewSecretKey(passphrase, config.N, config.R, config.P)
} }
// newSecretKey is used as a way to replace the new secret key generation var (
// function used so tests can provide a version that fails for testing error // secretKeyGen is the inner method that is executed when calling
// paths. // newSecretKey.
var newSecretKey = defaultNewSecretKey secretKeyGen = defaultNewSecretKey
// secretKeyGenMtx protects access to secretKeyGen, so that it can be
// replaced in testing.
secretKeyGenMtx sync.RWMutex
)
// SetSecretKeyGen replaces the existing secret key generator, and returns the
// previous generator.
func SetSecretKeyGen(keyGen SecretKeyGenerator) SecretKeyGenerator {
secretKeyGenMtx.Lock()
oldKeyGen := secretKeyGen
secretKeyGen = keyGen
secretKeyGenMtx.Unlock()
return oldKeyGen
}
// newSecretKey generates a new secret key using the active secretKeyGen.
func newSecretKey(passphrase *[]byte,
config *ScryptOptions) (*snacl.SecretKey, error) {
secretKeyGenMtx.RLock()
defer secretKeyGenMtx.RUnlock()
return secretKeyGen(passphrase, config)
}
// EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that // EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that
// our tests can use dependency injection to force the behaviour they need. // our tests can use dependency injection to force the behaviour they need.

View file

@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@ -33,6 +34,13 @@ func newHash(hexStr string) *chainhash.Hash {
return hash return hash
} }
// failingSecretKeyGen is a waddrmgr.SecretKeyGenerator that always returns
// snacl.ErrDecryptFailed.
func failingSecretKeyGen(passphrase *[]byte,
config *waddrmgr.ScryptOptions) (*snacl.SecretKey, error) {
return nil, snacl.ErrDecryptFailed
}
// testContext is used to store context information about a running test which // testContext is used to store context information about a running test which
// is passed into helper functions. The useSpends field indicates whether or // is passed into helper functions. The useSpends field indicates whether or
// not the spend data should be empty or figure it out based on the specific // not the spend data should be empty or figure it out based on the specific
@ -1099,14 +1107,12 @@ func testChangePassphrase(tc *testContext) bool {
// that intentionally errors. // that intentionally errors.
testName := "ChangePassphrase (public) with invalid new secret key" testName := "ChangePassphrase (public) with invalid new secret key"
var err error oldKeyGen := waddrmgr.SetSecretKeyGen(failingSecretKeyGen)
waddrmgr.TstRunWithReplacedNewSecretKey(func() { err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) return tc.rootManager.ChangePassphrase(
return tc.rootManager.ChangePassphrase( ns, pubPassphrase, pubPassphrase2, false, fastScrypt,
ns, pubPassphrase, pubPassphrase2, false, fastScrypt, )
)
})
}) })
if !checkManagerError(tc.t, testName, err, waddrmgr.ErrCrypto) { if !checkManagerError(tc.t, testName, err, waddrmgr.ErrCrypto) {
return false return false
@ -1114,6 +1120,7 @@ func testChangePassphrase(tc *testContext) bool {
// Attempt to change public passphrase with invalid old passphrase. // Attempt to change public passphrase with invalid old passphrase.
testName = "ChangePassphrase (public) with invalid old passphrase" testName = "ChangePassphrase (public) with invalid old passphrase"
waddrmgr.SetSecretKeyGen(oldKeyGen)
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return tc.rootManager.ChangePassphrase( return tc.rootManager.ChangePassphrase(

View file

@ -1434,33 +1434,62 @@ func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balan
// been used (there is at least one transaction spending to it in the // been used (there is at least one transaction spending to it in the
// blockchain or btcd mempool), the next chained address is returned. // blockchain or btcd mempool), the next chained address is returned.
func (w *Wallet) CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) { func (w *Wallet) CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) {
chainClient, err := w.requireChainClient()
if err != nil {
return nil, err
}
manager, err := w.Manager.FetchScopedKeyManager(scope) manager, err := w.Manager.FetchScopedKeyManager(scope)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var addr btcutil.Address var (
addr btcutil.Address
props *waddrmgr.AccountProperties
)
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
maddr, err := manager.LastExternalAddress(addrmgrNs, account) maddr, err := manager.LastExternalAddress(addrmgrNs, account)
if err != nil { if err != nil {
// If no address exists yet, create the first external address // If no address exists yet, create the first external
// address.
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
addr, err = w.newAddress(addrmgrNs, account, scope) addr, props, err = w.newAddress(
addrmgrNs, account, scope,
)
} }
return err return err
} }
// Get next chained address if the last one has already been used. // Get next chained address if the last one has already been
// used.
if maddr.Used(addrmgrNs) { if maddr.Used(addrmgrNs) {
addr, err = w.newAddress(addrmgrNs, account, scope) addr, props, err = w.newAddress(
addrmgrNs, account, scope,
)
return err return err
} }
addr = maddr.Address() addr = maddr.Address()
return nil return nil
}) })
return addr, err if err != nil {
return nil, err
}
// If the props have been initially, then we had to create a new address
// to satisfy the query. Notify the rpc server about the new address.
if props != nil {
err = chainClient.NotifyReceived([]btcutil.Address{addr})
if err != nil {
return nil, err
}
w.NtfnServer.notifyAccountProperties(props)
}
return addr, nil
} }
// PubKeyForAddress looks up the associated public key for a P2PKH address. // PubKeyForAddress looks up the associated public key for a P2PKH address.
@ -2763,69 +2792,94 @@ func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) {
// NewAddress returns the next external chained address for a wallet. // NewAddress returns the next external chained address for a wallet.
func (w *Wallet) NewAddress(account uint32, func (w *Wallet) NewAddress(account uint32,
scope waddrmgr.KeyScope) (addr btcutil.Address, err error) { scope waddrmgr.KeyScope) (btcutil.Address, error) {
chainClient, err := w.requireChainClient()
if err != nil {
return nil, err
}
var (
addr btcutil.Address
props *waddrmgr.AccountProperties
)
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
var err error var err error
addr, err = w.newAddress(addrmgrNs, account, scope) addr, props, err = w.newAddress(addrmgrNs, account, scope)
return err return err
}) })
return if err != nil {
return nil, err
}
// Notify the rpc server about the newly created address.
err = chainClient.NotifyReceived([]btcutil.Address{addr})
if err != nil {
return nil, err
}
w.NtfnServer.notifyAccountProperties(props)
return addr, nil
} }
func (w *Wallet) newAddress(addrmgrNs walletdb.ReadWriteBucket, account uint32, func (w *Wallet) newAddress(addrmgrNs walletdb.ReadWriteBucket, account uint32,
scope waddrmgr.KeyScope) (btcutil.Address, error) { scope waddrmgr.KeyScope) (btcutil.Address, *waddrmgr.AccountProperties, error) {
manager, err := w.Manager.FetchScopedKeyManager(scope) manager, err := w.Manager.FetchScopedKeyManager(scope)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// Get next address from wallet. // Get next address from wallet.
addrs, err := manager.NextExternalAddresses(addrmgrNs, account, 1) addrs, err := manager.NextExternalAddresses(addrmgrNs, account, 1)
if err != nil { if err != nil {
return nil, err return nil, nil, err
}
// Request updates from btcd for new transactions sent to this address.
utilAddrs := make([]btcutil.Address, len(addrs))
for i, addr := range addrs {
utilAddrs[i] = addr.Address()
}
w.chainClientLock.Lock()
chainClient := w.chainClient
w.chainClientLock.Unlock()
if chainClient != nil {
err := chainClient.NotifyReceived(utilAddrs)
if err != nil {
return nil, err
}
} }
props, err := manager.AccountProperties(addrmgrNs, account) props, err := manager.AccountProperties(addrmgrNs, account)
if err != nil { if err != nil {
log.Errorf("Cannot fetch account properties for notification "+ log.Errorf("Cannot fetch account properties for notification "+
"after deriving next external address: %v", err) "after deriving next external address: %v", err)
} else { return nil, nil, err
w.NtfnServer.notifyAccountProperties(props)
} }
return utilAddrs[0], nil return addrs[0].Address(), props, nil
} }
// NewChangeAddress returns a new change address for a wallet. // NewChangeAddress returns a new change address for a wallet.
func (w *Wallet) NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (addr btcutil.Address, err error) { func (w *Wallet) NewChangeAddress(account uint32,
scope waddrmgr.KeyScope) (btcutil.Address, error) {
chainClient, err := w.requireChainClient()
if err != nil {
return nil, err
}
var addr btcutil.Address
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
var err error var err error
addr, err = w.newChangeAddress(addrmgrNs, account) addr, err = w.newChangeAddress(addrmgrNs, account)
return err return err
}) })
return if err != nil {
return nil, err
}
// Notify the rpc server about the newly created address.
err = chainClient.NotifyReceived([]btcutil.Address{addr})
if err != nil {
return nil, err
}
return addr, nil
} }
func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket, account uint32) (btcutil.Address, error) { func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket,
account uint32) (btcutil.Address, error) {
// As we're making a change address, we'll fetch the type of manager // As we're making a change address, we'll fetch the type of manager
// that is able to make p2wkh output as they're the most efficient. // that is able to make p2wkh output as they're the most efficient.
scopes := w.Manager.ScopesForExternalAddrType( scopes := w.Manager.ScopesForExternalAddrType(
@ -2842,21 +2896,7 @@ func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket, account ui
return nil, err return nil, err
} }
// Request updates from btcd for new transactions sent to this address. return addrs[0].Address(), nil
utilAddrs := make([]btcutil.Address, len(addrs))
for i, addr := range addrs {
utilAddrs[i] = addr.Address()
}
chainClient, err := w.requireChainClient()
if err == nil {
err = chainClient.NotifyReceived(utilAddrs)
if err != nil {
return nil, err
}
}
return utilAddrs[0], nil
} }
// confirmed checks whether a transaction at height txHeight has met minconf // confirmed checks whether a transaction at height txHeight has met minconf