multi-account: support BIP44 account discovery
This commit is contained in:
parent
0410b7ce01
commit
169abd446c
4 changed files with 255 additions and 252 deletions
|
@ -42,6 +42,9 @@ const (
|
||||||
// ImportedAddrAccountName is the name of the imported account.
|
// ImportedAddrAccountName is the name of the imported account.
|
||||||
ImportedAddrAccountName = "imported"
|
ImportedAddrAccountName = "imported"
|
||||||
|
|
||||||
|
// AccountGapLimit is used for account discovery defined in BIP0044
|
||||||
|
AccountGapLimit = 20
|
||||||
|
|
||||||
// DefaultAccountNum is the number of the default account.
|
// DefaultAccountNum is the number of the default account.
|
||||||
DefaultAccountNum = 0
|
DefaultAccountNum = 0
|
||||||
|
|
||||||
|
@ -1527,7 +1530,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
|
||||||
|
|
||||||
// Derive the account key for the first account according our
|
// Derive the account key for the first account according our
|
||||||
// BIP0044-like derivation.
|
// BIP0044-like derivation.
|
||||||
acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0)
|
acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, DefaultAccountNum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The seed is unusable if the any of the children in the
|
// The seed is unusable if the any of the children in the
|
||||||
// required hierarchy can't be derived due to invalid child.
|
// required hierarchy can't be derived due to invalid child.
|
||||||
|
|
|
@ -116,11 +116,15 @@ type KeyScope struct {
|
||||||
// identify a particular child key, when the account and branch can be inferred
|
// identify a particular child key, when the account and branch can be inferred
|
||||||
// from context.
|
// from context.
|
||||||
type ScopedIndex struct {
|
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.
|
func (i ScopedIndex) String() string {
|
||||||
Index uint32
|
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
|
// String returns a human readable version describing the keypath encapsulated
|
||||||
|
@ -625,6 +629,14 @@ func (s *ScopedKeyManager) DeriveFromKeyPathCache(
|
||||||
return privKey, nil
|
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
|
// 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
|
// scheme) from a given key path. If key derivation isn't possible, then an
|
||||||
// error will be returned.
|
// 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.
|
// This function MUST be called with the manager lock held for writes.
|
||||||
func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket,
|
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
|
// The next address can only be generated for accounts that have
|
||||||
// already been created.
|
// already been created.
|
||||||
acctInfo, err := s.loadAccountInfo(ns, account)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1472,10 +1501,42 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastAccount, err := fetchLastAccount(ns, &s.scope)
|
||||||
|
if account < lastAccount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Save last account metadata
|
// Save last account metadata
|
||||||
return putLastAccount(ns, &s.scope, account)
|
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
|
// 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
|
// account number with the given name. If an account with the same name
|
||||||
// already exists, ErrDuplicateAccount will be returned.
|
// already exists, ErrDuplicateAccount will be returned.
|
||||||
|
|
|
@ -63,66 +63,45 @@ func (rm *RecoveryManager) Resurrect(ns walletdb.ReadBucket,
|
||||||
// First, for each scope that we are recovering, rederive all of the
|
// First, for each scope that we are recovering, rederive all of the
|
||||||
// addresses up to the last found address known to each branch.
|
// addresses up to the last found address known to each branch.
|
||||||
for keyScope, scopedMgr := range scopedMgrs {
|
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)
|
scopeState := rm.state.StateForScope(keyScope)
|
||||||
acctProperties, err := scopedMgr.AccountProperties(
|
|
||||||
ns, waddrmgr.DefaultAccountNum,
|
lastAccount, err := scopedMgr.LastAccount(ns)
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the external key count, which bounds the indexes we
|
for accountIndex, accountState := range scopeState[:lastAccount+1] {
|
||||||
// will need to rederive.
|
log.Infof("Resurrecting addresses for key scope %v, account %v", keyScope, accountIndex)
|
||||||
externalCount := acctProperties.ExternalKeyCount
|
acctProperties, err := scopedMgr.AccountProperties(ns,
|
||||||
|
uint32(accountIndex))
|
||||||
// Walk through all indexes through the last external key,
|
if err != nil {
|
||||||
// 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 {
|
|
||||||
return err
|
return err
|
||||||
} else if err == hdkeychain.ErrInvalidChild {
|
|
||||||
scopeState.ExternalBranch.MarkInvalidChild(i)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scopeState.ExternalBranch.AddAddr(i, addr.Address())
|
// Fetch the key count, which bounds the indexes we
|
||||||
}
|
// will need to rederive.
|
||||||
|
counts := []uint32{
|
||||||
// Fetch the internal key count, which bounds the indexes we
|
acctProperties.ExternalKeyCount,
|
||||||
// will need to rederive.
|
acctProperties.InternalKeyCount,
|
||||||
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())
|
for branchIndex, branchState := range accountState {
|
||||||
}
|
// Walk through all indexes through the last key,
|
||||||
|
// deriving each address and adding it to the branch
|
||||||
// The key counts will point to the next key that can be
|
// recovery state's set of addresses to look for.
|
||||||
// derived, so we subtract one to point to last known key. If
|
for addrIndex := uint32(0); addrIndex < counts[branchIndex]; addrIndex++ {
|
||||||
// the key count is zero, then no addresses have been found.
|
keyPath := keyPath(uint32(accountIndex), uint32(branchIndex), addrIndex)
|
||||||
if externalCount > 0 {
|
addr, err := scopedMgr.DeriveFromKeyPath(ns, keyPath)
|
||||||
scopeState.ExternalBranch.ReportFound(externalCount - 1)
|
if err != nil && err != hdkeychain.ErrInvalidChild {
|
||||||
}
|
return err
|
||||||
if internalCount > 0 {
|
} else if err == hdkeychain.ErrInvalidChild {
|
||||||
scopeState.InternalBranch.ReportFound(internalCount - 1)
|
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
|
// scopes maintains a map of each requested key scope to its active
|
||||||
// RecoveryState.
|
// RecoveryState.
|
||||||
scopes map[waddrmgr.KeyScope]*ScopeRecoveryState
|
scopes map[waddrmgr.KeyScope]ScopeRecoveryState
|
||||||
|
|
||||||
// watchedOutPoints contains the set of all outpoints known to the
|
// watchedOutPoints contains the set of all outpoints known to the
|
||||||
// wallet. This is updated iteratively as new outpoints are found during
|
// 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
|
// recoveryWindow. Each RecoveryState that is subsequently initialized for a
|
||||||
// particular key scope will receive the same recoveryWindow.
|
// particular key scope will receive the same recoveryWindow.
|
||||||
func NewRecoveryState(recoveryWindow uint32) *RecoveryState {
|
func NewRecoveryState(recoveryWindow uint32) *RecoveryState {
|
||||||
scopes := make(map[waddrmgr.KeyScope]*ScopeRecoveryState)
|
scopes := make(map[waddrmgr.KeyScope]ScopeRecoveryState)
|
||||||
|
|
||||||
return &RecoveryState{
|
return &RecoveryState{
|
||||||
recoveryWindow: recoveryWindow,
|
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
|
// does not already exist, a new one will be generated with the RecoveryState's
|
||||||
// recoveryWindow.
|
// recoveryWindow.
|
||||||
func (rs *RecoveryState) StateForScope(
|
func (rs *RecoveryState) StateForScope(
|
||||||
keyScope waddrmgr.KeyScope) *ScopeRecoveryState {
|
keyScope waddrmgr.KeyScope) ScopeRecoveryState {
|
||||||
|
|
||||||
// If the account recovery state already exists, return it.
|
scopeState, ok := rs.scopes[keyScope]
|
||||||
if scopeState, ok := rs.scopes[keyScope]; ok {
|
if !ok {
|
||||||
return scopeState
|
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
|
return scopeState
|
||||||
// chosen recovery window.
|
|
||||||
rs.scopes[keyScope] = NewScopeRecoveryState(rs.recoveryWindow)
|
|
||||||
|
|
||||||
return rs.scopes[keyScope]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchedOutPoints returns the global set of outpoints that are known to belong
|
// 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
|
// ScopeRecoveryState is used to manage the recovery of addresses generated
|
||||||
// under a particular BIP32 account. Each account tracks both an external and
|
// under a BIP32 accounts. Each account tracks both an external and internal
|
||||||
// internal branch recovery state, both of which use the same recovery window.
|
// branch recovery state, both of which use the same recovery window.
|
||||||
type ScopeRecoveryState struct {
|
type ScopeRecoveryState []AccountRecoveryState
|
||||||
// ExternalBranch is the recovery state of addresses generated for
|
|
||||||
// external use, i.e. receiving addresses.
|
|
||||||
AccountBranches [][2]*BranchRecoveryState
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewScopeRecoveryState initializes an ScopeRecoveryState with the chosen
|
type AccountRecoveryState []*BranchRecoveryState
|
||||||
// recovery window.
|
|
||||||
func NewScopeRecoveryState(recoveryWindow uint32) *ScopeRecoveryState {
|
|
||||||
return &ScopeRecoveryState{
|
|
||||||
ExternalBranch: NewBranchRecoveryState(recoveryWindow),
|
|
||||||
InternalBranch: NewBranchRecoveryState(recoveryWindow),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BranchRecoveryState maintains the required state in-order to properly
|
// BranchRecoveryState maintains the required state in-order to properly
|
||||||
// recover addresses derived from a particular account's internal or external
|
// recover addresses derived from a particular account's internal or external
|
||||||
|
|
308
wallet/wallet.go
308
wallet/wallet.go
|
@ -26,6 +26,7 @@ import (
|
||||||
btcutil "github.com/lbryio/lbcutil"
|
btcutil "github.com/lbryio/lbcutil"
|
||||||
"github.com/lbryio/lbcutil/hdkeychain"
|
"github.com/lbryio/lbcutil/hdkeychain"
|
||||||
"github.com/lbryio/lbcwallet/chain"
|
"github.com/lbryio/lbcwallet/chain"
|
||||||
|
"github.com/lbryio/lbcwallet/internal/prompt"
|
||||||
"github.com/lbryio/lbcwallet/waddrmgr"
|
"github.com/lbryio/lbcwallet/waddrmgr"
|
||||||
"github.com/lbryio/lbcwallet/wallet/txauthor"
|
"github.com/lbryio/lbcwallet/wallet/txauthor"
|
||||||
"github.com/lbryio/lbcwallet/wallet/txrules"
|
"github.com/lbryio/lbcwallet/wallet/txrules"
|
||||||
|
@ -666,16 +667,13 @@ func (w *Wallet) recovery(chainClient chain.Interface,
|
||||||
w.recoveryWindow, recoveryBatchSize, w.chainParams,
|
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)
|
scopedMgrs := make(map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager)
|
||||||
for _, scopedMgr := range w.Manager.ActiveScopedKeyManagers() {
|
for _, scopedMgr := range w.Manager.ActiveScopedKeyManagers() {
|
||||||
scopedMgrs[scopedMgr.Scope()] = scopedMgr
|
scopedMgrs[scopedMgr.Scope()] = scopedMgr
|
||||||
}
|
}
|
||||||
|
|
||||||
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
||||||
|
var credits []wtxmgr.Credit
|
||||||
txMgrNS := tx.ReadBucket(wtxmgrNamespaceKey)
|
txMgrNS := tx.ReadBucket(wtxmgrNamespaceKey)
|
||||||
credits, err := w.TxStore.UnspentOutputs(txMgrNS)
|
credits, err := w.TxStore.UnspentOutputs(txMgrNS)
|
||||||
if err != nil {
|
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
|
// 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
|
// that a wallet rescan will be performed from the wallet's tip, which
|
||||||
// will be of bestHeight after completing the recovery process.
|
// 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
|
var blocks []*waddrmgr.BlockStamp
|
||||||
startHeight := w.Manager.SyncedTo().Height + 1
|
startHeight := w.Manager.SyncedTo().Height + 1
|
||||||
for height := startHeight; height <= bestHeight; height++ {
|
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
|
// the recovery batch size, so we can proceed to commit our
|
||||||
// state to disk.
|
// state to disk.
|
||||||
recoveryBatch := recoveryMgr.BlockBatch()
|
recoveryBatch := recoveryMgr.BlockBatch()
|
||||||
if len(recoveryBatch) == recoveryBatchSize || height == bestHeight {
|
if len(recoveryBatch) != recoveryBatchSize && height != bestHeight {
|
||||||
err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
continue
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
|
@ -795,16 +813,8 @@ func (w *Wallet) recoverScopedAddresses(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Scanning %d blocks for recoverable addresses", len(batch))
|
|
||||||
|
|
||||||
expandHorizons:
|
expandHorizons:
|
||||||
for scope, scopedMgr := range scopedMgrs {
|
log.Infof("Scanning %d blocks for recoverable addresses", len(batch))
|
||||||
scopeState := recoveryState.StateForScope(scope)
|
|
||||||
err := expandScopeHorizons(ns, scopedMgr, scopeState)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the internal and external horizons properly expanded, we now
|
// With the internal and external horizons properly expanded, we now
|
||||||
// construct the filter blocks request. The request includes the range
|
// 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
|
// 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
|
// horizon will be properly extended such that our lookahead always includes the
|
||||||
// proper number of valid child keys.
|
// proper number of valid child keys.
|
||||||
func expandScopeHorizons(ns walletdb.ReadWriteBucket,
|
func expandScopeHorizons(
|
||||||
|
ns walletdb.ReadWriteBucket,
|
||||||
scopedMgr *waddrmgr.ScopedKeyManager,
|
scopedMgr *waddrmgr.ScopedKeyManager,
|
||||||
scopeState *ScopeRecoveryState) error {
|
scopeState ScopeRecoveryState) error {
|
||||||
|
|
||||||
// Compute the current external horizon and the number of addresses we
|
for accountIndex, accountState := range scopeState {
|
||||||
// must derive to ensure we maintain a sufficient recovery window for
|
acctKey, err := scopedMgr.DeriveAccountKey(ns, uint32(accountIndex))
|
||||||
// the external branch.
|
if err != nil {
|
||||||
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:
|
|
||||||
return err
|
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
|
branchKey, err := acctKey.Derive(uint32(branchIndex))
|
||||||
// with the external branch recovery state.
|
if err != nil {
|
||||||
scopeState.ExternalBranch.AddAddr(childIndex, addr.Address())
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
childIndex++
|
for count < exWindow {
|
||||||
count++
|
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
|
case err != nil:
|
||||||
// must derive to ensure we maintain a sufficient recovery window for
|
return err
|
||||||
// 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:
|
branchState.AddAddr(addrIndex, addr.Address())
|
||||||
return err
|
|
||||||
|
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
|
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 {
|
func keyPath(account, branch, index uint32) waddrmgr.DerivationPath {
|
||||||
return waddrmgr.DerivationPath{
|
return waddrmgr.DerivationPath{
|
||||||
InternalAccount: account,
|
InternalAccount: account,
|
||||||
|
@ -976,23 +967,22 @@ func newFilterBlocksRequest(batch []wtxmgr.BlockMeta,
|
||||||
WatchedOutPoints: recoveryState.WatchedOutPoints(),
|
WatchedOutPoints: recoveryState.WatchedOutPoints(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the external and internal addresses by merging the addresses
|
// Populate the addresses by merging the addresses sets belong to all
|
||||||
// sets belong to all currently tracked scopes.
|
// currently tracked scopes.
|
||||||
for scope := range scopedMgrs {
|
for scope := range scopedMgrs {
|
||||||
scopeState := recoveryState.StateForScope(scope)
|
scopeState := recoveryState.StateForScope(scope)
|
||||||
for index, addr := range scopeState.ExternalBranch.Addrs() {
|
for accountIndex, accountState := range scopeState {
|
||||||
scopedIndex := waddrmgr.ScopedIndex{
|
for branchIndex, branchState := range accountState {
|
||||||
Scope: scope,
|
for addrIndex, addr := range branchState.Addrs() {
|
||||||
Index: index,
|
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,
|
scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager,
|
||||||
recoveryState *RecoveryState) error {
|
recoveryState *RecoveryState) error {
|
||||||
|
|
||||||
// Mark all recovered external addresses as used. This will be done only
|
// Mark all recovered addresses as used. This will be done only for
|
||||||
// for scopes that reported a non-zero number of external addresses in
|
// scopes that reported a non-zero number of addresses in this block.
|
||||||
// this block.
|
for index := range filterResp.FoundAddresses {
|
||||||
for scope, indexes := range filterResp.FoundExternalAddrs {
|
scopedMgr := scopedMgrs[index.Scope]
|
||||||
// First, report all external child indexes found for this
|
// First, report all child indexes found for this scope. This
|
||||||
// scope. This ensures that the external last-found index will
|
// ensures that the last-found index will be updated to include
|
||||||
// be updated to include the maximum child index seen thus far.
|
// the maximum child index seen thus far.
|
||||||
scopeState := recoveryState.StateForScope(scope)
|
scopeState := recoveryState.StateForScope(index.Scope)
|
||||||
for index := range indexes {
|
branchState := scopeState[index.Account][index.Branch]
|
||||||
scopeState.ExternalBranch.ReportFound(index)
|
branchState.ReportFound(index.Index)
|
||||||
}
|
|
||||||
|
|
||||||
scopedMgr := scopedMgrs[scope]
|
|
||||||
|
|
||||||
// Now, with all found addresses reported, derive and extend all
|
// Now, with all found addresses reported, derive and extend all
|
||||||
// external addresses up to and including the current last found
|
// external addresses up to and including the current last found
|
||||||
// index for this scope.
|
// index for this scope.
|
||||||
exNextUnfound := scopeState.ExternalBranch.NextUnfound()
|
nextFound := branchState.NextUnfound()
|
||||||
|
|
||||||
exLastFound := exNextUnfound
|
lastFound := nextFound
|
||||||
if exLastFound > 0 {
|
if lastFound > 0 {
|
||||||
exLastFound--
|
lastFound--
|
||||||
}
|
}
|
||||||
|
|
||||||
err := scopedMgr.ExtendExternalAddresses(
|
err := scopedMgr.ExtendAddresses(
|
||||||
ns, waddrmgr.DefaultAccountNum, exLastFound,
|
ns, index.Account, index.Branch, lastFound,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, with the scope's addresses extended, we mark used
|
// Finally, with the scope's addresses extended, we mark used
|
||||||
// the external addresses that were found in the block and
|
// the addresses that were found in the block and belong to
|
||||||
// belong to this scope.
|
// this scope.
|
||||||
for index := range indexes {
|
addr := branchState.GetAddr(index.Index)
|
||||||
addr := scopeState.ExternalBranch.GetAddr(index)
|
err = scopedMgr.MarkUsed(ns, addr)
|
||||||
err := scopedMgr.MarkUsed(ns, addr)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark all recovered internal addresses as used. This will be done only
|
var lastAccount uint32
|
||||||
// for scopes that reported a non-zero number of internal addresses in
|
for _, scopedMgr := range scopedMgrs {
|
||||||
// this block.
|
account, err := scopedMgr.LastAccount(ns)
|
||||||
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,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if lastAccount < account {
|
||||||
|
lastAccount = account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, with the scope's addresses extended, we mark used
|
// Make sure all scopes are extended to the same account.
|
||||||
// the internal addresses that were found in the blockand belong
|
for _, s := range scopedMgrs {
|
||||||
// to this scope.
|
for gapAccount := lastAccount; gapAccount >= 0; gapAccount-- {
|
||||||
for index := range indexes {
|
_, err := s.AccountProperties(ns, gapAccount)
|
||||||
addr := scopeState.InternalBranch.GetAddr(index)
|
// If the account exists, we can stop extending.
|
||||||
err := scopedMgr.MarkUsed(ns, addr)
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = s.NewRawAccount(ns, gapAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue