Skip to content

Commit

Permalink
multi-account: support BIP44 account discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
roylee17 committed Sep 26, 2022
1 parent b774170 commit cd179e0
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 250 deletions.
5 changes: 4 additions & 1 deletion waddrmgr/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
71 changes: 66 additions & 5 deletions waddrmgr/scoped_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
125 changes: 48 additions & 77 deletions wallet/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {

// If the account recovery state already exists, return it.
if scopeState, ok := rs.scopes[keyScope]; ok {
return scopeState
keyScope waddrmgr.KeyScope) ScopeRecoveryState {

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
Expand All @@ -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
Expand Down
Loading

0 comments on commit cd179e0

Please sign in to comment.