multi-account: support BIP44 account discovery

This commit is contained in:
Roy Lee 2022-09-16 20:10:47 -07:00
parent 0410b7ce01
commit 169abd446c
4 changed files with 255 additions and 252 deletions

View file

@ -42,6 +42,9 @@ const (
// ImportedAddrAccountName is the name of the imported account.
ImportedAddrAccountName = "imported"
// AccountGapLimit is used for account discovery defined in BIP0044
AccountGapLimit = 20
// DefaultAccountNum is the number of the default account.
DefaultAccountNum = 0
@ -1527,7 +1530,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
// Derive the account key for the first account according our
// BIP0044-like derivation.
acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0)
acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, DefaultAccountNum)
if err != nil {
// The seed is unusable if the any of the children in the
// required hierarchy can't be derived due to invalid child.

View file

@ -116,13 +116,17 @@ type KeyScope struct {
// identify a particular child key, when the account and branch can be inferred
// from context.
type ScopedIndex struct {
// Scope is the BIP44 account' used to derive the child key.
Scope KeyScope
// Index is the BIP44 address_index used to derive the child key.
Account uint32
Branch uint32
Index uint32
}
func (i ScopedIndex) String() string {
return fmt.Sprintf("%s/%d'/%d/%d",
i.Scope, i.Account, i.Branch, i.Index)
}
// String returns a human readable version describing the keypath encapsulated
// by the target key scope.
func (k KeyScope) String() string {
@ -625,6 +629,14 @@ func (s *ScopedKeyManager) DeriveFromKeyPathCache(
return privKey, nil
}
func (s *ScopedKeyManager) DeriveFromExtKeys(kp DerivationPath,
derivedKey *hdkeychain.ExtendedKey,
addrType AddressType) (ManagedAddress, error) {
return newManagedAddressFromExtKey(
s, kp, derivedKey, addrType,
)
}
// DeriveFromKeyPath attempts to derive a maximal child key (under the BIP0044
// scheme) from a given key path. If key derivation isn't possible, then an
// error will be returned.
@ -1105,11 +1117,28 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket,
//
// This function MUST be called with the manager lock held for writes.
func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket,
account uint32, lastIndex uint32, internal bool) error {
account uint32, branch uint32, lastIndex uint32) error {
// The next address can only be generated for accounts that have
// already been created.
acctInfo, err := s.loadAccountInfo(ns, account)
if err != nil {
err = s.newAccount(ns, account, fmt.Sprintf("act:%v", account))
if err != nil {
return err
}
for gapAccount := account - 1; gapAccount >= 0; gapAccount-- {
_, err = s.loadAccountInfo(ns, gapAccount)
if err == nil {
break
}
err = s.newAccount(ns, gapAccount, fmt.Sprintf("act:%v", gapAccount))
if err != nil {
return err
}
}
}
acctInfo, err = s.loadAccountInfo(ns, account)
if err != nil {
return err
}
@ -1472,10 +1501,42 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket,
return err
}
lastAccount, err := fetchLastAccount(ns, &s.scope)
if account < lastAccount {
return nil
}
// Save last account metadata
return putLastAccount(ns, &s.scope, account)
}
func (s *ScopedKeyManager) DeriveAccountKey(ns walletdb.ReadWriteBucket,
account uint32) (*hdkeychain.ExtendedKey, error) {
_, coinTypePrivEnc, err := fetchCoinTypeKeys(ns, &s.scope)
if err != nil {
return nil, err
}
// Decrypt the cointype key.
serializedKeyPriv, err := s.rootManager.cryptoKeyPriv.Decrypt(coinTypePrivEnc)
if err != nil {
str := fmt.Sprintf("failed to decrypt cointype serialized private key")
return nil, managerError(ErrLocked, str, err)
}
defer zero.Bytes(serializedKeyPriv)
coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv))
if err != nil {
str := fmt.Sprintf("failed to create cointype extended private key")
return nil, managerError(ErrKeyChain, str, err)
}
defer coinTypeKeyPriv.Zero()
// Derive the account key using the cointype key
return deriveAccountKey(coinTypeKeyPriv, account)
}
// RenameAccount renames an account stored in the manager based on the given
// account number with the given name. If an account with the same name
// already exists, ErrDuplicateAccount will be returned.

View file

@ -63,66 +63,45 @@ func (rm *RecoveryManager) Resurrect(ns walletdb.ReadBucket,
// First, for each scope that we are recovering, rederive all of the
// addresses up to the last found address known to each branch.
for keyScope, scopedMgr := range scopedMgrs {
// Load the current account properties for this scope, using the
// the default account number.
// TODO(conner): rescan for all created accounts if we allow
// users to use non-default address
scopeState := rm.state.StateForScope(keyScope)
acctProperties, err := scopedMgr.AccountProperties(
ns, waddrmgr.DefaultAccountNum,
)
lastAccount, err := scopedMgr.LastAccount(ns)
if err != nil {
return err
}
// Fetch the external key count, which bounds the indexes we
// will need to rederive.
externalCount := acctProperties.ExternalKeyCount
for accountIndex, accountState := range scopeState[:lastAccount+1] {
log.Infof("Resurrecting addresses for key scope %v, account %v", keyScope, accountIndex)
acctProperties, err := scopedMgr.AccountProperties(ns,
uint32(accountIndex))
if err != nil {
return err
}
// Walk through all indexes through the last external key,
// deriving each address and adding it to the external branch
// Fetch the key count, which bounds the indexes we
// will need to rederive.
counts := []uint32{
acctProperties.ExternalKeyCount,
acctProperties.InternalKeyCount,
}
for branchIndex, branchState := range accountState {
// Walk through all indexes through the last key,
// deriving each address and adding it to the branch
// recovery state's set of addresses to look for.
for i := uint32(0); i < externalCount; i++ {
keyPath := externalKeyPath(i)
for addrIndex := uint32(0); addrIndex < counts[branchIndex]; addrIndex++ {
keyPath := keyPath(uint32(accountIndex), uint32(branchIndex), addrIndex)
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
if err != nil && err != hdkeychain.ErrInvalidChild {
return err
} else if err == hdkeychain.ErrInvalidChild {
scopeState.ExternalBranch.MarkInvalidChild(i)
branchState.MarkInvalidChild(addrIndex)
continue
}
scopeState.ExternalBranch.AddAddr(i, addr.Address())
branchState.AddAddr(addrIndex, addr.Address())
}
// Fetch the internal key count, which bounds the indexes we
// will need to rederive.
internalCount := acctProperties.InternalKeyCount
// Walk through all indexes through the last internal key,
// deriving each address and adding it to the internal branch
// recovery state's set of addresses to look for.
for i := uint32(0); i < internalCount; i++ {
keyPath := internalKeyPath(i)
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
if err != nil && err != hdkeychain.ErrInvalidChild {
return err
} else if err == hdkeychain.ErrInvalidChild {
scopeState.InternalBranch.MarkInvalidChild(i)
continue
}
scopeState.InternalBranch.AddAddr(i, addr.Address())
}
// The key counts will point to the next key that can be
// derived, so we subtract one to point to last known key. If
// the key count is zero, then no addresses have been found.
if externalCount > 0 {
scopeState.ExternalBranch.ReportFound(externalCount - 1)
}
if internalCount > 0 {
scopeState.InternalBranch.ReportFound(internalCount - 1)
}
}
@ -202,7 +181,7 @@ type RecoveryState struct {
// scopes maintains a map of each requested key scope to its active
// RecoveryState.
scopes map[waddrmgr.KeyScope]*ScopeRecoveryState
scopes map[waddrmgr.KeyScope]ScopeRecoveryState
// watchedOutPoints contains the set of all outpoints known to the
// wallet. This is updated iteratively as new outpoints are found during
@ -214,7 +193,7 @@ type RecoveryState struct {
// recoveryWindow. Each RecoveryState that is subsequently initialized for a
// particular key scope will receive the same recoveryWindow.
func NewRecoveryState(recoveryWindow uint32) *RecoveryState {
scopes := make(map[waddrmgr.KeyScope]*ScopeRecoveryState)
scopes := make(map[waddrmgr.KeyScope]ScopeRecoveryState)
return &RecoveryState{
recoveryWindow: recoveryWindow,
@ -227,18 +206,21 @@ func NewRecoveryState(recoveryWindow uint32) *RecoveryState {
// does not already exist, a new one will be generated with the RecoveryState's
// recoveryWindow.
func (rs *RecoveryState) StateForScope(
keyScope waddrmgr.KeyScope) *ScopeRecoveryState {
keyScope waddrmgr.KeyScope) ScopeRecoveryState {
// If the account recovery state already exists, return it.
if scopeState, ok := rs.scopes[keyScope]; ok {
return scopeState
scopeState, ok := rs.scopes[keyScope]
if !ok {
for i := 0; i < waddrmgr.AccountGapLimit; i++ {
accountState := []*BranchRecoveryState{
NewBranchRecoveryState(rs.recoveryWindow),
NewBranchRecoveryState(rs.recoveryWindow),
}
scopeState = append(scopeState, accountState)
}
rs.scopes[keyScope] = scopeState
}
// Otherwise, initialize the recovery state for this scope with the
// chosen recovery window.
rs.scopes[keyScope] = NewScopeRecoveryState(rs.recoveryWindow)
return rs.scopes[keyScope]
return scopeState
}
// WatchedOutPoints returns the global set of outpoints that are known to belong
@ -256,22 +238,11 @@ func (rs *RecoveryState) AddWatchedOutPoint(outPoint *wire.OutPoint,
}
// ScopeRecoveryState is used to manage the recovery of addresses generated
// under a particular BIP32 account. Each account tracks both an external and
// internal branch recovery state, both of which use the same recovery window.
type ScopeRecoveryState struct {
// ExternalBranch is the recovery state of addresses generated for
// external use, i.e. receiving addresses.
AccountBranches [][2]*BranchRecoveryState
}
// under a BIP32 accounts. Each account tracks both an external and internal
// branch recovery state, both of which use the same recovery window.
type ScopeRecoveryState []AccountRecoveryState
// NewScopeRecoveryState initializes an ScopeRecoveryState with the chosen
// recovery window.
func NewScopeRecoveryState(recoveryWindow uint32) *ScopeRecoveryState {
return &ScopeRecoveryState{
ExternalBranch: NewBranchRecoveryState(recoveryWindow),
InternalBranch: NewBranchRecoveryState(recoveryWindow),
}
}
type AccountRecoveryState []*BranchRecoveryState
// BranchRecoveryState maintains the required state in-order to properly
// recover addresses derived from a particular account's internal or external

View file

@ -26,6 +26,7 @@ import (
btcutil "github.com/lbryio/lbcutil"
"github.com/lbryio/lbcutil/hdkeychain"
"github.com/lbryio/lbcwallet/chain"
"github.com/lbryio/lbcwallet/internal/prompt"
"github.com/lbryio/lbcwallet/waddrmgr"
"github.com/lbryio/lbcwallet/wallet/txauthor"
"github.com/lbryio/lbcwallet/wallet/txrules"
@ -666,16 +667,13 @@ func (w *Wallet) recovery(chainClient chain.Interface,
w.recoveryWindow, recoveryBatchSize, w.chainParams,
)
// In the event that this recovery is being resumed, we will need to
// repopulate all found addresses from the database. Ideally, for basic
// recovery, we would only do so for the default scopes, but due to a
// bug in which the wallet would create change addresses outside of the
// default scopes, it's necessary to attempt all registered key scopes.
scopedMgrs := make(map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager)
for _, scopedMgr := range w.Manager.ActiveScopedKeyManagers() {
scopedMgrs[scopedMgr.Scope()] = scopedMgr
}
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
var credits []wtxmgr.Credit
txMgrNS := tx.ReadBucket(wtxmgrNamespaceKey)
credits, err := w.TxStore.UnspentOutputs(txMgrNS)
if err != nil {
@ -704,6 +702,18 @@ func (w *Wallet) recovery(chainClient chain.Interface,
// NOTE: We purposefully don't update our best height since we assume
// that a wallet rescan will be performed from the wallet's tip, which
// will be of bestHeight after completing the recovery process.
pass, err := prompt.ProvidePrivPassphrase()
if err != nil {
return err
}
err = w.Unlock(pass, nil)
if err != nil {
return err
}
defer w.Lock()
var blocks []*waddrmgr.BlockStamp
startHeight := w.Manager.SyncedTo().Height + 1
for height := startHeight; height <= bestHeight; height++ {
@ -735,18 +745,27 @@ func (w *Wallet) recovery(chainClient chain.Interface,
// the recovery batch size, so we can proceed to commit our
// state to disk.
recoveryBatch := recoveryMgr.BlockBatch()
if len(recoveryBatch) == recoveryBatchSize || height == bestHeight {
err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
if len(recoveryBatch) != recoveryBatchSize && height != bestHeight {
continue
}
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
for _, block := range blocks {
err := w.Manager.SetSyncedTo(ns, block)
err = w.Manager.SetSyncedTo(ns, block)
if err != nil {
return err
}
}
return w.recoverScopedAddresses(
chainClient, tx, ns, recoveryBatch,
recoveryMgr.State(), scopedMgrs,
for scope, scopedMgr := range scopedMgrs {
scopeState := recoveryMgr.State().StateForScope(scope)
err = expandScopeHorizons(ns, scopedMgr, scopeState)
if err != nil {
return err
}
}
return w.recoverScopedAddresses(chainClient, tx, ns,
recoveryBatch, recoveryMgr.State(), scopedMgrs,
)
})
if err != nil {
@ -764,7 +783,6 @@ func (w *Wallet) recovery(chainClient chain.Interface,
blocks = blocks[:0]
recoveryMgr.ResetBlockBatch()
}
}
return nil
}
@ -795,16 +813,8 @@ func (w *Wallet) recoverScopedAddresses(
return nil
}
log.Infof("Scanning %d blocks for recoverable addresses", len(batch))
expandHorizons:
for scope, scopedMgr := range scopedMgrs {
scopeState := recoveryState.StateForScope(scope)
err := expandScopeHorizons(ns, scopedMgr, scopeState)
if err != nil {
return err
}
}
log.Infof("Scanning %d blocks for recoverable addresses", len(batch))
// With the internal and external horizons properly expanded, we now
// construct the filter blocks request. The request includes the range
@ -887,74 +897,55 @@ expandHorizons:
// persistent state of the wallet. If any invalid child keys are detected, the
// horizon will be properly extended such that our lookahead always includes the
// proper number of valid child keys.
func expandScopeHorizons(ns walletdb.ReadWriteBucket,
func expandScopeHorizons(
ns walletdb.ReadWriteBucket,
scopedMgr *waddrmgr.ScopedKeyManager,
scopeState *ScopeRecoveryState) error {
scopeState ScopeRecoveryState) error {
for accountIndex, accountState := range scopeState {
acctKey, err := scopedMgr.DeriveAccountKey(ns, uint32(accountIndex))
if err != nil {
return err
}
for branchIndex, branchState := range accountState {
exHorizon, exWindow := branchState.ExtendHorizon()
count, addrIndex := uint32(0), exHorizon
branchKey, err := acctKey.Derive(uint32(branchIndex))
if err != nil {
return err
}
// Compute the current external horizon and the number of addresses we
// must derive to ensure we maintain a sufficient recovery window for
// the external branch.
exHorizon, exWindow := scopeState.ExternalBranch.ExtendHorizon()
count, childIndex := uint32(0), exHorizon
for count < exWindow {
keyPath := externalKeyPath(childIndex)
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
kp := keyPath(uint32(accountIndex), uint32(branchIndex), addrIndex)
indexKey, err := branchKey.Derive(addrIndex)
if err != nil {
return err
}
addrType := waddrmgr.ScopeAddrMap[scopedMgr.Scope()].ExternalAddrType
addr, err := scopedMgr.DeriveFromExtKeys(kp, indexKey, addrType)
switch {
case err == hdkeychain.ErrInvalidChild:
// Record the existence of an invalid child with the
// external branch's recovery state. This also
// increments the branch's horizon so that it accounts
// for this skipped child index.
scopeState.ExternalBranch.MarkInvalidChild(childIndex)
childIndex++
branchState.MarkInvalidChild(addrIndex)
addrIndex++
continue
case err != nil:
return err
}
// Register the newly generated external address and child index
// with the external branch recovery state.
scopeState.ExternalBranch.AddAddr(childIndex, addr.Address())
branchState.AddAddr(addrIndex, addr.Address())
childIndex++
addrIndex++
count++
}
// Compute the current internal horizon and the number of addresses we
// must derive to ensure we maintain a sufficient recovery window for
// the internal branch.
inHorizon, inWindow := scopeState.InternalBranch.ExtendHorizon()
count, childIndex = 0, inHorizon
for count < inWindow {
keyPath := internalKeyPath(childIndex)
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
switch {
case err == hdkeychain.ErrInvalidChild:
// Record the existence of an invalid child with the
// internal branch's recovery state. This also
// increments the branch's horizon so that it accounts
// for this skipped child index.
scopeState.InternalBranch.MarkInvalidChild(childIndex)
childIndex++
continue
case err != nil:
return err
}
// Register the newly generated internal address and child index
// with the internal branch recovery state.
scopeState.InternalBranch.AddAddr(childIndex, addr.Address())
childIndex++
count++
}
return nil
}
// keyPath returns the relative external derivation path /account/branch/index.
// keyPath returns the relative derivation path /account/branch/index.
func keyPath(account, branch, index uint32) waddrmgr.DerivationPath {
return waddrmgr.DerivationPath{
InternalAccount: account,
@ -976,23 +967,22 @@ func newFilterBlocksRequest(batch []wtxmgr.BlockMeta,
WatchedOutPoints: recoveryState.WatchedOutPoints(),
}
// Populate the external and internal addresses by merging the addresses
// sets belong to all currently tracked scopes.
// Populate the addresses by merging the addresses sets belong to all
// currently tracked scopes.
for scope := range scopedMgrs {
scopeState := recoveryState.StateForScope(scope)
for index, addr := range scopeState.ExternalBranch.Addrs() {
for accountIndex, accountState := range scopeState {
for branchIndex, branchState := range accountState {
for addrIndex, addr := range branchState.Addrs() {
scopedIndex := waddrmgr.ScopedIndex{
Scope: scope,
Index: index,
Account: uint32(accountIndex),
Branch: uint32(branchIndex),
Index: addrIndex,
}
filterReq.ExternalAddrs[scopedIndex] = addr
filterReq.Addresses[scopedIndex] = addr
}
for index, addr := range scopeState.InternalBranch.Addrs() {
scopedIndex := waddrmgr.ScopedIndex{
Scope: scope,
Index: index,
}
filterReq.InternalAddrs[scopedIndex] = addr
}
}
@ -1007,85 +997,63 @@ func extendFoundAddresses(ns walletdb.ReadWriteBucket,
scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager,
recoveryState *RecoveryState) error {
// Mark all recovered external addresses as used. This will be done only
// for scopes that reported a non-zero number of external addresses in
// this block.
for scope, indexes := range filterResp.FoundExternalAddrs {
// First, report all external child indexes found for this
// scope. This ensures that the external last-found index will
// be updated to include the maximum child index seen thus far.
scopeState := recoveryState.StateForScope(scope)
for index := range indexes {
scopeState.ExternalBranch.ReportFound(index)
}
scopedMgr := scopedMgrs[scope]
// Mark all recovered addresses as used. This will be done only for
// scopes that reported a non-zero number of addresses in this block.
for index := range filterResp.FoundAddresses {
scopedMgr := scopedMgrs[index.Scope]
// First, report all child indexes found for this scope. This
// ensures that the last-found index will be updated to include
// the maximum child index seen thus far.
scopeState := recoveryState.StateForScope(index.Scope)
branchState := scopeState[index.Account][index.Branch]
branchState.ReportFound(index.Index)
// Now, with all found addresses reported, derive and extend all
// external addresses up to and including the current last found
// index for this scope.
exNextUnfound := scopeState.ExternalBranch.NextUnfound()
nextFound := branchState.NextUnfound()
exLastFound := exNextUnfound
if exLastFound > 0 {
exLastFound--
lastFound := nextFound
if lastFound > 0 {
lastFound--
}
err := scopedMgr.ExtendExternalAddresses(
ns, waddrmgr.DefaultAccountNum, exLastFound,
err := scopedMgr.ExtendAddresses(
ns, index.Account, index.Branch, lastFound,
)
if err != nil {
return err
}
// Finally, with the scope's addresses extended, we mark used
// the external addresses that were found in the block and
// belong to this scope.
for index := range indexes {
addr := scopeState.ExternalBranch.GetAddr(index)
err := scopedMgr.MarkUsed(ns, addr)
// the addresses that were found in the block and belong to
// this scope.
addr := branchState.GetAddr(index.Index)
err = scopedMgr.MarkUsed(ns, addr)
if err != nil {
return err
}
}
}
// Mark all recovered internal addresses as used. This will be done only
// for scopes that reported a non-zero number of internal addresses in
// this block.
for scope, indexes := range filterResp.FoundInternalAddrs {
// First, report all internal child indexes found for this
// scope. This ensures that the internal last-found index will
// be updated to include the maximum child index seen thus far.
scopeState := recoveryState.StateForScope(scope)
for index := range indexes {
scopeState.InternalBranch.ReportFound(index)
}
scopedMgr := scopedMgrs[scope]
// Now, with all found addresses reported, derive and extend all
// internal addresses up to and including the current last found
// index for this scope.
inNextUnfound := scopeState.InternalBranch.NextUnfound()
inLastFound := inNextUnfound
if inLastFound > 0 {
inLastFound--
}
err := scopedMgr.ExtendInternalAddresses(
ns, waddrmgr.DefaultAccountNum, inLastFound,
)
var lastAccount uint32
for _, scopedMgr := range scopedMgrs {
account, err := scopedMgr.LastAccount(ns)
if err != nil {
return err
}
if lastAccount < account {
lastAccount = account
}
}
// Finally, with the scope's addresses extended, we mark used
// the internal addresses that were found in the blockand belong
// to this scope.
for index := range indexes {
addr := scopeState.InternalBranch.GetAddr(index)
err := scopedMgr.MarkUsed(ns, addr)
// Make sure all scopes are extended to the same account.
for _, s := range scopedMgrs {
for gapAccount := lastAccount; gapAccount >= 0; gapAccount-- {
_, err := s.AccountProperties(ns, gapAccount)
// If the account exists, we can stop extending.
if err == nil {
break
}
err = s.NewRawAccount(ns, gapAccount)
if err != nil {
return err
}