From 169abd446c8386e4747489333a2a5403e6d147d6 Mon Sep 17 00:00:00 2001 From: Roy Lee Date: Fri, 16 Sep 2022 20:10:47 -0700 Subject: [PATCH] multi-account: support BIP44 account discovery --- waddrmgr/manager.go | 5 +- waddrmgr/scoped_manager.go | 71 ++++++++- wallet/recovery.go | 123 ++++++--------- wallet/wallet.go | 308 +++++++++++++++++-------------------- 4 files changed, 255 insertions(+), 252 deletions(-) diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index d49efeb..44b36fa 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -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. diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index cc243d5..726f4f7 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -116,11 +116,15 @@ 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 + Scope KeyScope + Account uint32 + Branch uint32 + Index uint32 +} - // Index is the BIP44 address_index used to derive the child key. - 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 @@ -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. diff --git a/wallet/recovery.go b/wallet/recovery.go index 0f885f3..f6352af 100644 --- a/wallet/recovery.go +++ b/wallet/recovery.go @@ -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 - - // Walk through all indexes through the last external key, - // deriving each address and adding it to the external branch - // recovery state's set of addresses to look for. - for i := uint32(0); i < externalCount; i++ { - keyPath := externalKeyPath(i) - addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath) - if err != nil && err != hdkeychain.ErrInvalidChild { + 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 - } else if err == hdkeychain.ErrInvalidChild { - scopeState.ExternalBranch.MarkInvalidChild(i) - continue } - scopeState.ExternalBranch.AddAddr(i, 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 + // Fetch the key count, which bounds the indexes we + // will need to rederive. + counts := []uint32{ + acctProperties.ExternalKeyCount, + acctProperties.InternalKeyCount, } - 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) + 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 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 { + branchState.MarkInvalidChild(addrIndex) + continue + } + branchState.AddAddr(addrIndex, addr.Address()) + } + } } } @@ -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 diff --git a/wallet/wallet.go b/wallet/wallet.go index aa240f7..464a14e 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -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,35 +745,43 @@ 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 { - ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) - for _, block := range blocks { - err := w.Manager.SetSyncedTo(ns, block) - if err != nil { - return err - } - } - return w.recoverScopedAddresses( - chainClient, tx, ns, recoveryBatch, - recoveryMgr.State(), scopedMgrs, - ) - }) - if err != nil { - return err - } - - if len(recoveryBatch) > 0 { - log.Infof("Recovered addresses from blocks "+ - "%d-%d", recoveryBatch[0].Height, - recoveryBatch[len(recoveryBatch)-1].Height) - } - - // Clear the batch of all processed blocks to reuse the - // same memory for future batches. - blocks = blocks[:0] - recoveryMgr.ResetBlockBatch() + 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) + if err != nil { + return err + } + } + 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 { + return err + } + + if len(recoveryBatch) > 0 { + log.Infof("Recovered addresses from blocks "+ + "%d-%d", recoveryBatch[0].Height, + recoveryBatch[len(recoveryBatch)-1].Height) + } + + // Clear the batch of all processed blocks to reuse the + // same memory for future batches. + 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 { - // 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) - 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++ - continue - - case err != nil: + 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 - // Register the newly generated external address and child index - // with the external branch recovery state. - scopeState.ExternalBranch.AddAddr(childIndex, addr.Address()) + branchKey, err := acctKey.Derive(uint32(branchIndex)) + if err != nil { + return err + } - childIndex++ - count++ - } + for count < exWindow { + 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: + branchState.MarkInvalidChild(addrIndex) + addrIndex++ + continue - // 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 + } - case err != nil: - return err + branchState.AddAddr(addrIndex, addr.Address()) + + addrIndex++ + count++ + } } - - // 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() { - scopedIndex := waddrmgr.ScopedIndex{ - Scope: scope, - Index: index, + for accountIndex, accountState := range scopeState { + for branchIndex, branchState := range accountState { + for addrIndex, addr := range branchState.Addrs() { + scopedIndex := waddrmgr.ScopedIndex{ + Scope: scope, + Account: uint32(accountIndex), + Branch: uint32(branchIndex), + Index: addrIndex, + } + filterReq.Addresses[scopedIndex] = addr + } } - filterReq.ExternalAddrs[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) - if err != nil { - return err - } + // 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 }