diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 28f9975..2b72cda 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -122,3 +122,1364 @@ var ( } ) +// ScopedKeyManager is a sub key manager under the main root key manager. The +// root key manager will handle the root HD key (m/), while each sub scoped key +// manager will handle the cointype key for a particular key scope +// (m/purpose'/cointype'). This abstraction allows higher-level applications +// built upon the root key manager to perform their own arbitrary key +// derivation, while still being protected under the encryption of the root key +// manager. +type ScopedKeyManager struct { + // scope is the scope of this key manager. We can only generate keys + // that are direct children of this scope. + scope KeyScope + + // addrSchema is the address schema for this sub manager. This will be + // consulted when encoding addresses from derived keys. + addrSchema ScopeAddrSchema + + // rootManager is a pointer to the root key manager. We'll maintain + // this as we need access to the crypto encryption keys before we can + // derive any new accounts of child keys of accounts. + rootManager *Manager + + // addrs is a cached map of all the addresses that we currently + // manager. + addrs map[addrKey]ManagedAddress + + // acctInfo houses information about accounts including what is needed + // to generate deterministic chained keys for each created account. + acctInfo map[uint32]*accountInfo + + // deriveOnUnlock is a list of private keys which needs to be derived + // on the next unlock. This occurs when a public address is derived + // while the address manager is locked since it does not have access to + // the private extended key (hence nor the underlying private key) in + // order to encrypt it. + deriveOnUnlock []*unlockDeriveInfo + + mtx sync.RWMutex +} + +// Scope returns the exact KeyScope of this scoped key manager. +func (s *ScopedKeyManager) Scope() KeyScope { + return s.scope +} + +// AddrSchema returns the set address schema for the target ScopedKeyManager. +func (s *ScopedKeyManager) AddrSchema() ScopeAddrSchema { + return s.addrSchema +} + +// zeroSensitivePublicData performs a best try effort to remove and zero all +// sensitive public data associated with the address manager such as +// hierarchical deterministic extended public keys and the crypto public keys. +func (s *ScopedKeyManager) zeroSensitivePublicData() { + // Clear all of the account private keys. + for _, acctInfo := range s.acctInfo { + acctInfo.acctKeyPub.Zero() + acctInfo.acctKeyPub = nil + } +} + +// Close cleanly shuts down the manager. It makes a best try effort to remove +// and zero all private key and sensitive public key material associated with +// the address manager from memory. +func (s *ScopedKeyManager) Close() { + s.mtx.Lock() + defer s.mtx.Unlock() + + // Attempt to clear sensitive public key material from memory too. + s.zeroSensitivePublicData() + return +} + +// keyToManaged returns a new managed address for the provided derived key and +// its derivation path which consists of the account, branch, and index. +// +// The passed derivedKey is zeroed after the new address is created. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey, + account, branch, index uint32) (ManagedAddress, error) { + + var addrType AddressType + if branch == internalBranch { + addrType = s.addrSchema.InternalAddrType + } else { + addrType = s.addrSchema.ExternalAddrType + } + + // Create a new managed address based on the public or private key + // depending on whether the passed key is private. Also, zero the key + // after creating the managed address from it. + ma, err := newManagedAddressFromExtKey( + s, account, derivedKey, addrType, + ) + defer derivedKey.Zero() + if err != nil { + return nil, err + } + + if !derivedKey.IsPrivate() { + // Add the managed address to the list of addresses that need + // their private keys derived when the address manager is next + // unlocked. + info := unlockDeriveInfo{ + managedAddr: ma, + branch: branch, + index: index, + } + s.deriveOnUnlock = append(s.deriveOnUnlock, &info) + } + + if branch == internalBranch { + ma.internal = true + } + + return ma, nil +} + +// deriveKey returns either a public or private derived extended key based on +// the private flag for the given an account info, branch, and index. +func (s *ScopedKeyManager) deriveKey(acctInfo *accountInfo, branch, + index uint32, private bool) (*hdkeychain.ExtendedKey, error) { + + // Choose the public or private extended key based on whether or not + // the private flag was specified. This, in turn, allows for public or + // private child derivation. + acctKey := acctInfo.acctKeyPub + if private { + acctKey = acctInfo.acctKeyPriv + } + + // Derive and return the key. + branchKey, err := acctKey.Child(branch) + if err != nil { + str := fmt.Sprintf("failed to derive extended key branch %d", + branch) + return nil, managerError(ErrKeyChain, str, err) + } + + addressKey, err := branchKey.Child(index) + branchKey.Zero() // Zero branch key after it's used. + if err != nil { + str := fmt.Sprintf("failed to derive child extended key -- "+ + "branch %d, child %d", + branch, index) + return nil, managerError(ErrKeyChain, str, err) + } + + return addressKey, nil +} + +// loadAccountInfo attempts to load and cache information about the given +// account from the database. This includes what is necessary to derive new +// keys for it and track the state of the internal and external branches. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, + account uint32) (*accountInfo, error) { + + // Return the account info from cache if it's available. + if acctInfo, ok := s.acctInfo[account]; ok { + return acctInfo, nil + } + + // The account is either invalid or just wasn't cached, so attempt to + // load the information from the database. + rowInterface, err := fetchAccountInfo(ns, &s.scope, account) + if err != nil { + return nil, maybeConvertDbError(err) + } + + // Ensure the account type is a default account. + row, ok := rowInterface.(*dbDefaultAccountRow) + if !ok { + str := fmt.Sprintf("unsupported account type %T", row) + err = managerError(ErrDatabase, str, nil) + } + + // Use the crypto public key to decrypt the account public extended + // key. + serializedKeyPub, err := s.rootManager.cryptoKeyPub.Decrypt(row.pubKeyEncrypted) + if err != nil { + str := fmt.Sprintf("failed to decrypt public key for account %d", + account) + return nil, managerError(ErrCrypto, str, err) + } + acctKeyPub, err := hdkeychain.NewKeyFromString(string(serializedKeyPub)) + if err != nil { + str := fmt.Sprintf("failed to create extended public key for "+ + "account %d", account) + return nil, managerError(ErrKeyChain, str, err) + } + + // Create the new account info with the known information. The rest of + // the fields are filled out below. + acctInfo := &accountInfo{ + acctName: row.name, + acctKeyEncrypted: row.privKeyEncrypted, + acctKeyPub: acctKeyPub, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + } + + if !s.rootManager.Locked() { + // Use the crypto private key to decrypt the account private + // extended keys. + decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) + if err != nil { + str := fmt.Sprintf("failed to decrypt private key for "+ + "account %d", account) + return nil, managerError(ErrCrypto, str, err) + } + + acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted)) + if err != nil { + str := fmt.Sprintf("failed to create extended private "+ + "key for account %d", account) + return nil, managerError(ErrKeyChain, str, err) + } + acctInfo.acctKeyPriv = acctKeyPriv + } + + // Derive and cache the managed address for the last external address. + branch, index := externalBranch, row.nextExternalIndex + if index > 0 { + index-- + } + lastExtKey, err := s.deriveKey( + acctInfo, branch, index, !s.rootManager.Locked(), + ) + if err != nil { + return nil, err + } + lastExtAddr, err := s.keyToManaged(lastExtKey, account, branch, index) + if err != nil { + return nil, err + } + acctInfo.lastExternalAddr = lastExtAddr + + // Derive and cache the managed address for the last internal address. + branch, index = internalBranch, row.nextInternalIndex + if index > 0 { + index-- + } + lastIntKey, err := s.deriveKey( + acctInfo, branch, index, !s.rootManager.Locked(), + ) + if err != nil { + return nil, err + } + lastIntAddr, err := s.keyToManaged(lastIntKey, account, branch, index) + if err != nil { + return nil, err + } + acctInfo.lastInternalAddr = lastIntAddr + + // Add it to the cache and return it when everything is successful. + s.acctInfo[account] = acctInfo + return acctInfo, nil +} + +// AccountProperties returns properties associated with the account, such as +// the account number, name, and the number of derived and imported keys. +func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, + account uint32) (*AccountProperties, error) { + + defer s.mtx.RUnlock() + s.mtx.RLock() + + props := &AccountProperties{AccountNumber: account} + + // Until keys can be imported into any account, special handling is + // required for the imported account. + // + // loadAccountInfo errors when using it on the imported account since + // the accountInfo struct is filled with a BIP0044 account's extended + // keys, and the imported accounts has none. + // + // Since only the imported account allows imports currently, the number + // of imported keys for any other account is zero, and since the + // imported account cannot contain non-imported keys, the external and + // internal key counts for it are zero. + if account != ImportedAddrAccount { + acctInfo, err := s.loadAccountInfo(ns, account) + if err != nil { + return nil, err + } + props.AccountName = acctInfo.acctName + props.ExternalKeyCount = acctInfo.nextExternalIndex + props.InternalKeyCount = acctInfo.nextInternalIndex + } else { + props.AccountName = ImportedAddrAccountName // reserved, nonchangable + + // Could be more efficient if this was tracked by the db. + var importedKeyCount uint32 + count := func(interface{}) error { + importedKeyCount++ + return nil + } + err := forEachAccountAddress(ns, &s.scope, ImportedAddrAccount, count) + if err != nil { + return nil, err + } + props.ImportedKeyCount = importedKeyCount + } + + return props, nil +} + +// 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. +func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket, + kp DerivationPath) (ManagedAddress, error) { + + s.mtx.Lock() + defer s.mtx.Unlock() + + extKey, err := s.deriveKeyFromPath( + ns, kp.Account, kp.Branch, kp.Index, !s.rootManager.Locked(), + ) + if err != nil { + return nil, err + } + + return s.keyToManaged(extKey, kp.Account, kp.Branch, kp.Index) +} + +// deriveKeyFromPath returns either a public or private derived extended key +// based on the private flag for the given an account, branch, and index. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket, account, branch, + index uint32, private bool) (*hdkeychain.ExtendedKey, error) { + + // Look up the account key information. + acctInfo, err := s.loadAccountInfo(ns, account) + if err != nil { + return nil, err + } + + return s.deriveKey(acctInfo, branch, index, private) +} + +// chainAddressRowToManaged returns a new managed address based on chained +// address data loaded from the database. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket, + row *dbChainAddressRow) (ManagedAddress, error) { + + addressKey, err := s.deriveKeyFromPath( + ns, row.account, row.branch, row.index, !s.rootManager.Locked(), + ) + if err != nil { + return nil, err + } + + return s.keyToManaged(addressKey, row.account, row.branch, row.index) +} + +// importedAddressRowToManaged returns a new managed address based on imported +// address data loaded from the database. +func (s *ScopedKeyManager) importedAddressRowToManaged(row *dbImportedAddressRow) (ManagedAddress, error) { + + // Use the crypto public key to decrypt the imported public key. + pubBytes, err := s.rootManager.cryptoKeyPub.Decrypt(row.encryptedPubKey) + if err != nil { + str := "failed to decrypt public key for imported address" + return nil, managerError(ErrCrypto, str, err) + } + + pubKey, err := btcec.ParsePubKey(pubBytes, btcec.S256()) + if err != nil { + str := "invalid public key for imported address" + return nil, managerError(ErrCrypto, str, err) + } + + compressed := len(pubBytes) == btcec.PubKeyBytesLenCompressed + ma, err := newManagedAddressWithoutPrivKey( + s, row.account, pubKey, compressed, + s.addrSchema.ExternalAddrType, + ) + if err != nil { + return nil, err + } + ma.privKeyEncrypted = row.encryptedPrivKey + ma.imported = true + + return ma, nil +} + +// scriptAddressRowToManaged returns a new managed address based on script +// address data loaded from the database. +func (s *ScopedKeyManager) scriptAddressRowToManaged(row *dbScriptAddressRow) (ManagedAddress, error) { + // Use the crypto public key to decrypt the imported script hash. + scriptHash, err := s.rootManager.cryptoKeyPub.Decrypt(row.encryptedHash) + if err != nil { + str := "failed to decrypt imported script hash" + return nil, managerError(ErrCrypto, str, err) + } + + return newScriptAddress(s, row.account, scriptHash, row.encryptedScript) +} + +// rowInterfaceToManaged returns a new managed address based on the given +// address data loaded from the database. It will automatically select the +// appropriate type. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) rowInterfaceToManaged(ns walletdb.ReadBucket, + rowInterface interface{}) (ManagedAddress, error) { + + switch row := rowInterface.(type) { + case *dbChainAddressRow: + return s.chainAddressRowToManaged(ns, row) + + case *dbImportedAddressRow: + return s.importedAddressRowToManaged(row) + + case *dbScriptAddressRow: + return s.scriptAddressRowToManaged(row) + } + + str := fmt.Sprintf("unsupported address type %T", rowInterface) + return nil, managerError(ErrDatabase, str, nil) +} + +// loadAndCacheAddress attempts to load the passed address from the database +// and caches the associated managed address. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) loadAndCacheAddress(ns walletdb.ReadBucket, + address btcutil.Address) (ManagedAddress, error) { + + // Attempt to load the raw address information from the database. + rowInterface, err := fetchAddress(ns, &s.scope, address.ScriptAddress()) + if err != nil { + if merr, ok := err.(*ManagerError); ok { + desc := fmt.Sprintf("failed to fetch address '%s': %v", + address.ScriptAddress(), merr.Description) + merr.Description = desc + return nil, merr + } + return nil, maybeConvertDbError(err) + } + + // Create a new managed address for the specific type of address based + // on type. + managedAddr, err := s.rowInterfaceToManaged(ns, rowInterface) + if err != nil { + return nil, err + } + + // Cache and return the new managed address. + s.addrs[addrKey(managedAddr.Address().ScriptAddress())] = managedAddr + + return managedAddr, nil +} + +// existsAddress returns whether or not the passed address is known to the +// address manager. +// +// This function MUST be called with the manager lock held for reads. +func (s *ScopedKeyManager) existsAddress(ns walletdb.ReadBucket, addressID []byte) bool { + // Check the in-memory map first since it's faster than a db access. + if _, ok := s.addrs[addrKey(addressID)]; ok { + return true + } + + // Check the database if not already found above. + return existsAddress(ns, &s.scope, addressID) +} + +// Address returns a managed address given the passed address if it is known to +// the address manager. A managed address differs from the passed address in +// that it also potentially contains extra information needed to sign +// transactions such as the associated private key for pay-to-pubkey and +// pay-to-pubkey-hash addresses and the script associated with +// pay-to-script-hash addresses. +func (s *ScopedKeyManager) Address(ns walletdb.ReadBucket, + address btcutil.Address) (ManagedAddress, error) { + + // ScriptAddress will only return a script hash if we're accessing an + // address that is either PKH or SH. In the event we're passed a PK + // address, convert the PK to PKH address so that we can access it from + // the addrs map and database. + if pka, ok := address.(*btcutil.AddressPubKey); ok { + address = pka.AddressPubKeyHash() + } + + // Return the address from cache if it's available. + // + // NOTE: Not using a defer on the lock here since a write lock is + // needed if the lookup fails. + s.mtx.RLock() + if ma, ok := s.addrs[addrKey(address.ScriptAddress())]; ok { + s.mtx.RUnlock() + return ma, nil + } + s.mtx.RUnlock() + + s.mtx.Lock() + defer s.mtx.Unlock() + + // Attempt to load the address from the database. + return s.loadAndCacheAddress(ns, address) +} + +// AddrAccount returns the account to which the given address belongs. +func (s *ScopedKeyManager) AddrAccount(ns walletdb.ReadBucket, + address btcutil.Address) (uint32, error) { + + account, err := fetchAddrAccount(ns, &s.scope, address.ScriptAddress()) + if err != nil { + return 0, maybeConvertDbError(err) + } + + return account, nil +} + +// nextAddresses returns the specified number of next chained address from the +// branch indicated by the internal flag. +// +// This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, + account uint32, numAddresses uint32, internal bool) ([]ManagedAddress, error) { + + // The next address can only be generated for accounts that have + // already been created. + acctInfo, err := s.loadAccountInfo(ns, account) + if err != nil { + return nil, err + } + + // Choose the account key to used based on whether the address manager + // is locked. + acctKey := acctInfo.acctKeyPub + if !s.rootManager.Locked() { + acctKey = acctInfo.acctKeyPriv + } + + // Choose the branch key and index depending on whether or not this is + // an internal address. + branchNum, nextIndex := externalBranch, acctInfo.nextExternalIndex + if internal { + branchNum = internalBranch + nextIndex = acctInfo.nextInternalIndex + } + + addrType := s.addrSchema.ExternalAddrType + if internal { + addrType = s.addrSchema.InternalAddrType + } + + // Ensure the requested number of addresses doesn't exceed the maximum + // allowed for this account. + if numAddresses > MaxAddressesPerAccount || nextIndex+numAddresses > + MaxAddressesPerAccount { + str := fmt.Sprintf("%d new addresses would exceed the maximum "+ + "allowed number of addresses per account of %d", + numAddresses, MaxAddressesPerAccount) + return nil, managerError(ErrTooManyAddresses, str, nil) + } + + // Derive the appropriate branch key and ensure it is zeroed when done. + branchKey, err := acctKey.Child(branchNum) + if err != nil { + str := fmt.Sprintf("failed to derive extended key branch %d", + branchNum) + return nil, managerError(ErrKeyChain, str, err) + } + defer branchKey.Zero() // Ensure branch key is zeroed when done. + + // Create the requested number of addresses and keep track of the index + // with each one. + addressInfo := make([]*unlockDeriveInfo, 0, numAddresses) + for i := uint32(0); i < numAddresses; i++ { + // There is an extremely small chance that a particular child is + // invalid, so use a loop to derive the next valid child. + var nextKey *hdkeychain.ExtendedKey + for { + // Derive the next child in the external chain branch. + key, err := branchKey.Child(nextIndex) + if err != nil { + // When this particular child is invalid, skip to the + // next index. + if err == hdkeychain.ErrInvalidChild { + nextIndex++ + continue + } + + str := fmt.Sprintf("failed to generate child %d", + nextIndex) + return nil, managerError(ErrKeyChain, str, err) + } + key.SetNet(s.rootManager.chainParams) + + nextIndex++ + nextKey = key + break + } + + // Create a new managed address based on the public or private + // key depending on whether the generated key is private. + // Also, zero the next key after creating the managed address + // from it. + addr, err := newManagedAddressFromExtKey( + s, account, nextKey, addrType, + ) + if err != nil { + return nil, err + } + if internal { + addr.internal = true + } + managedAddr := addr + nextKey.Zero() + + info := unlockDeriveInfo{ + managedAddr: managedAddr, + branch: branchNum, + index: nextIndex - 1, + } + addressInfo = append(addressInfo, &info) + } + + // Now that all addresses have been successfully generated, update the + // database in a single transaction. + for _, info := range addressInfo { + ma := info.managedAddr + addressID := ma.Address().ScriptAddress() + + switch a := ma.(type) { + case *managedAddress: + err := putChainedAddress( + ns, &s.scope, addressID, account, ssFull, + info.branch, info.index, adtChain, + ) + if err != nil { + return nil, maybeConvertDbError(err) + } + case *scriptAddress: + encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(a.AddrHash()) + if err != nil { + str := fmt.Sprintf("failed to encrypt script hash %x", + a.AddrHash()) + return nil, managerError(ErrCrypto, str, err) + } + + err = putScriptAddress( + ns, &s.scope, a.AddrHash(), ImportedAddrAccount, + ssNone, encryptedHash, a.scriptEncrypted, + ) + if err != nil { + return nil, maybeConvertDbError(err) + } + } + } + + // Finally update the next address tracking and add the addresses to + // the cache after the newly generated addresses have been successfully + // added to the db. + managedAddresses := make([]ManagedAddress, 0, len(addressInfo)) + for _, info := range addressInfo { + ma := info.managedAddr + s.addrs[addrKey(ma.Address().ScriptAddress())] = ma + + // Add the new managed address to the list of addresses that + // need their private keys derived when the address manager is + // next unlocked. + if s.rootManager.Locked() && !s.rootManager.WatchOnly() { + s.deriveOnUnlock = append(s.deriveOnUnlock, info) + } + + managedAddresses = append(managedAddresses, ma) + } + + // Set the last address and next address for tracking. + ma := addressInfo[len(addressInfo)-1].managedAddr + if internal { + acctInfo.nextInternalIndex = nextIndex + acctInfo.lastInternalAddr = ma + } else { + acctInfo.nextExternalIndex = nextIndex + acctInfo.lastExternalAddr = ma + } + + return managedAddresses, nil +} + +// NextExternalAddresses returns the specified number of next chained addresses +// that are intended for external use from the address manager. +func (s *ScopedKeyManager) NextExternalAddresses(ns walletdb.ReadWriteBucket, + account uint32, numAddresses uint32) ([]ManagedAddress, error) { + + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + return s.nextAddresses(ns, account, numAddresses, false) +} + +// NextInternalAddresses returns the specified number of next chained addresses +// that are intended for internal use such as change from the address manager. +func (s *ScopedKeyManager) NextInternalAddresses(ns walletdb.ReadWriteBucket, + account uint32, numAddresses uint32) ([]ManagedAddress, error) { + + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + return s.nextAddresses(ns, account, numAddresses, true) +} + +// LastExternalAddress returns the most recently requested chained external +// address from calling NextExternalAddress for the given account. The first +// external address for the account will be returned if none have been +// previously requested. +// +// This function will return an error if the provided account number is greater +// than the MaxAccountNum constant or there is no account information for the +// passed account. Any other errors returned are generally unexpected. +func (s *ScopedKeyManager) LastExternalAddress(ns walletdb.ReadBucket, + account uint32) (ManagedAddress, error) { + + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + // Load account information for the passed account. It is typically + // cached, but if not it will be loaded from the database. + acctInfo, err := s.loadAccountInfo(ns, account) + if err != nil { + return nil, err + } + + if acctInfo.nextExternalIndex > 0 { + return acctInfo.lastExternalAddr, nil + } + + return nil, managerError(ErrAddressNotFound, "no previous external address", nil) +} + +// LastInternalAddress returns the most recently requested chained internal +// address from calling NextInternalAddress for the given account. The first +// internal address for the account will be returned if none have been +// previously requested. +// +// This function will return an error if the provided account number is greater +// than the MaxAccountNum constant or there is no account information for the +// passed account. Any other errors returned are generally unexpected. +func (s *ScopedKeyManager) LastInternalAddress(ns walletdb.ReadBucket, + account uint32) (ManagedAddress, error) { + + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + // Load account information for the passed account. It is typically + // cached, but if not it will be loaded from the database. + acctInfo, err := s.loadAccountInfo(ns, account) + if err != nil { + return nil, err + } + + if acctInfo.nextInternalIndex > 0 { + return acctInfo.lastInternalAddr, nil + } + + return nil, managerError(ErrAddressNotFound, "no previous internal address", nil) +} + +// NewRawAccount creates a new account for the scoped manager. This method +// differs from the NewAccount method in that this method takes the acount +// number *directly*, rather than taking a string name for the account, then +// mapping that to the next highest account number. +func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error { + if s.rootManager.WatchOnly() { + return managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + if s.rootManager.Locked() { + return managerError(ErrLocked, errLocked, nil) + } + + // As this is an ad hoc account that may not follow our normal linear + // derivation, we'll create a new name for this account based off of + // the account number. + name := fmt.Sprintf("act:%v", number) + return s.newAccount(ns, number, name) +} + +// NewAccount creates and returns a new account stored in the manager based on +// the given account name. If an account with the same name already exists, +// ErrDuplicateAccount will be returned. Since creating a new account requires +// access to the cointype keys (from which extended account keys are derived), +// it requires the manager to be unlocked. +func (s *ScopedKeyManager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) { + if s.rootManager.WatchOnly() { + return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + if s.rootManager.Locked() { + return 0, managerError(ErrLocked, errLocked, nil) + } + + // Fetch latest account, and create a new account in the same + // transaction Fetch the latest account number to generate the next + // account number + account, err := fetchLastAccount(ns, &s.scope) + if err != nil { + return 0, err + } + account++ + + // With the name validated, we'll create a new account for the new + // contiguous account. + if err := s.newAccount(ns, account, name); err != nil { + return 0, err + } + + return account, nil +} + +// newAccount is a helper function that derives a new precise account number, +// and creates a mapping from the passed name to the account number in the +// database. +// +// NOTE: This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, + account uint32, name string) error { + + // Validate the account name. + if err := ValidateAccountName(name); err != nil { + return err + } + + // Check that account with the same name does not exist + _, err := s.lookupAccount(ns, name) + if err == nil { + str := fmt.Sprintf("account with the same name already exists") + return managerError(ErrDuplicateAccount, str, err) + } + + // Fetch the cointype key which will be used to derive the next account + // extended keys + _, coinTypePrivEnc, err := fetchCoinTypeKeys(ns, &s.scope) + if err != nil { + return 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 managerError(ErrLocked, str, err) + } + coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv)) + zero.Bytes(serializedKeyPriv) + if err != nil { + str := fmt.Sprintf("failed to create cointype extended private key") + return managerError(ErrKeyChain, str, err) + } + + // Derive the account key using the cointype key + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account) + coinTypeKeyPriv.Zero() + if err != nil { + str := "failed to convert private key for account" + return managerError(ErrKeyChain, str, err) + } + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert public key for account" + return managerError(ErrKeyChain, str, err) + } + + // Encrypt the default account keys with the associated crypto keys. + acctPubEnc, err := s.rootManager.cryptoKeyPub.Encrypt( + []byte(acctKeyPub.String()), + ) + if err != nil { + str := "failed to encrypt public key for account" + return managerError(ErrCrypto, str, err) + } + acctPrivEnc, err := s.rootManager.cryptoKeyPriv.Encrypt( + []byte(acctKeyPriv.String()), + ) + if err != nil { + str := "failed to encrypt private key for account" + return managerError(ErrCrypto, str, err) + } + + // We have the encrypted account extended keys, so save them to the + // database + err = putAccountInfo( + ns, &s.scope, account, acctPubEnc, acctPrivEnc, 0, 0, name, + ) + if err != nil { + return err + } + + // Save last account metadata + return putLastAccount(ns, &s.scope, 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. +func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket, + account uint32, name string) error { + + s.mtx.Lock() + defer s.mtx.Unlock() + + // Ensure that a reserved account is not being renamed. + if isReservedAccountNum(account) { + str := "reserved account cannot be renamed" + return managerError(ErrInvalidAccount, str, nil) + } + + // Check that account with the new name does not exist + _, err := s.lookupAccount(ns, name) + if err == nil { + str := fmt.Sprintf("account with the same name already exists") + return managerError(ErrDuplicateAccount, str, err) + } + + // Validate account name + if err := ValidateAccountName(name); err != nil { + return err + } + + rowInterface, err := fetchAccountInfo(ns, &s.scope, account) + if err != nil { + return err + } + + // Ensure the account type is a default account. + row, ok := rowInterface.(*dbDefaultAccountRow) + if !ok { + str := fmt.Sprintf("unsupported account type %T", row) + err = managerError(ErrDatabase, str, nil) + } + + // Remove the old name key from the account id index. + if err = deleteAccountIDIndex(ns, &s.scope, account); err != nil { + return err + } + + // Remove the old name key from the account name index. + if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { + return err + } + err = putAccountInfo( + ns, &s.scope, account, row.pubKeyEncrypted, + row.privKeyEncrypted, row.nextExternalIndex, + row.nextInternalIndex, name, + ) + if err != nil { + return err + } + + // Update in-memory account info with new name if cached and the db + // write was successful. + if err == nil { + if acctInfo, ok := s.acctInfo[account]; ok { + acctInfo.acctName = name + } + } + + return err +} + +// ImportPrivateKey imports a WIF private key into the address manager. The +// imported address is created using either a compressed or uncompressed +// serialized public key, depending on the CompressPubKey bool of the WIF. +// +// All imported addresses will be part of the account defined by the +// ImportedAddrAccount constant. +// +// NOTE: When the address manager is watching-only, the private key itself will +// not be stored or available since it is private data. Instead, only the +// public key will be stored. This means it is paramount the private key is +// kept elsewhere as the watching-only address manager will NOT ever have access +// to it. +// +// This function will return an error if the address manager is locked and not +// watching-only, or not for the same network as the key trying to be imported. +// It will also return an error if the address already exists. Any other +// errors returned are generally unexpected. +func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, + wif *btcutil.WIF, bs *BlockStamp) (ManagedPubKeyAddress, error) { + + // Ensure the address is intended for network the address manager is + // associated with. + if !wif.IsForNet(s.rootManager.chainParams) { + str := fmt.Sprintf("private key is not for the same network the "+ + "address manager is configured for (%s)", + s.rootManager.chainParams.Name) + return nil, managerError(ErrWrongNet, str, nil) + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + // The manager must be unlocked to encrypt the imported private key. + if s.rootManager.Locked() && !s.rootManager.WatchOnly() { + return nil, managerError(ErrLocked, errLocked, nil) + } + + // Prevent duplicates. + serializedPubKey := wif.SerializePubKey() + pubKeyHash := btcutil.Hash160(serializedPubKey) + alreadyExists := s.existsAddress(ns, pubKeyHash) + if alreadyExists { + str := fmt.Sprintf("address for public key %x already exists", + serializedPubKey) + return nil, managerError(ErrDuplicateAddress, str, nil) + } + + // Encrypt public key. + encryptedPubKey, err := s.rootManager.cryptoKeyPub.Encrypt( + serializedPubKey, + ) + if err != nil { + str := fmt.Sprintf("failed to encrypt public key for %x", + serializedPubKey) + return nil, managerError(ErrCrypto, str, err) + } + + // Encrypt the private key when not a watching-only address manager. + var encryptedPrivKey []byte + if !s.rootManager.WatchOnly() { + privKeyBytes := wif.PrivKey.Serialize() + encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes) + zero.Bytes(privKeyBytes) + if err != nil { + str := fmt.Sprintf("failed to encrypt private key for %x", + serializedPubKey) + return nil, managerError(ErrCrypto, str, err) + } + } + + // The start block needs to be updated when the newly imported address + // is before the current one. + s.rootManager.mtx.Lock() + updateStartBlock := bs.Height < s.rootManager.syncState.startBlock.Height + s.rootManager.mtx.Unlock() + + // Save the new imported address to the db and update start block (if + // needed) in a single transaction. + err = putImportedAddress( + ns, &s.scope, pubKeyHash, ImportedAddrAccount, ssNone, + encryptedPubKey, encryptedPrivKey, + ) + if err != nil { + return nil, err + } + + if updateStartBlock { + err := putStartBlock(ns, bs) + if err != nil { + return nil, err + } + } + + // Now that the database has been updated, update the start block in + // memory too if needed. + if updateStartBlock { + s.rootManager.mtx.Lock() + s.rootManager.syncState.startBlock = *bs + s.rootManager.mtx.Unlock() + } + + // Create a new managed address based on the imported address. + var managedAddr *managedAddress + if !s.rootManager.WatchOnly() { + managedAddr, err = newManagedAddress( + s, ImportedAddrAccount, wif.PrivKey, + wif.CompressPubKey, s.addrSchema.ExternalAddrType, + ) + } else { + pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey) + managedAddr, err = newManagedAddressWithoutPrivKey( + s, ImportedAddrAccount, pubKey, wif.CompressPubKey, + s.addrSchema.ExternalAddrType, + ) + } + if err != nil { + return nil, err + } + managedAddr.imported = true + + // Add the new managed address to the cache of recent addresses and + // return it. + s.addrs[addrKey(managedAddr.Address().ScriptAddress())] = managedAddr + return managedAddr, nil +} + +// ImportScript imports a user-provided script into the address manager. The +// imported script will act as a pay-to-script-hash address. +// +// All imported script addresses will be part of the account defined by the +// ImportedAddrAccount constant. +// +// When the address manager is watching-only, the script itself will not be +// stored or available since it is considered private data. +// +// This function will return an error if the address manager is locked and not +// watching-only, or the address already exists. Any other errors returned are +// generally unexpected. +func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket, + script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { + + s.mtx.Lock() + defer s.mtx.Unlock() + + // The manager must be unlocked to encrypt the imported script. + if s.rootManager.Locked() && !s.rootManager.WatchOnly() { + return nil, managerError(ErrLocked, errLocked, nil) + } + + // Prevent duplicates. + scriptHash := btcutil.Hash160(script) + alreadyExists := s.existsAddress(ns, scriptHash) + if alreadyExists { + str := fmt.Sprintf("address for script hash %x already exists", + scriptHash) + return nil, managerError(ErrDuplicateAddress, str, nil) + } + + // Encrypt the script hash using the crypto public key so it is + // accessible when the address manager is locked or watching-only. + encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptHash) + if err != nil { + str := fmt.Sprintf("failed to encrypt script hash %x", + scriptHash) + return nil, managerError(ErrCrypto, str, err) + } + + // Encrypt the script for storage in database using the crypto script + // key when not a watching-only address manager. + var encryptedScript []byte + if !s.rootManager.WatchOnly() { + encryptedScript, err = s.rootManager.cryptoKeyScript.Encrypt( + script, + ) + if err != nil { + str := fmt.Sprintf("failed to encrypt script for %x", + scriptHash) + return nil, managerError(ErrCrypto, str, err) + } + } + + // The start block needs to be updated when the newly imported address + // is before the current one. + updateStartBlock := false + s.rootManager.mtx.Lock() + if bs.Height < s.rootManager.syncState.startBlock.Height { + updateStartBlock = true + } + s.rootManager.mtx.Unlock() + + // Save the new imported address to the db and update start block (if + // needed) in a single transaction. + err = putScriptAddress( + ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + encryptedHash, encryptedScript, + ) + if err != nil { + return nil, maybeConvertDbError(err) + } + + if updateStartBlock { + err := putStartBlock(ns, bs) + if err != nil { + return nil, maybeConvertDbError(err) + } + } + + // Now that the database has been updated, update the start block in + // memory too if needed. + if updateStartBlock { + s.rootManager.mtx.Lock() + s.rootManager.syncState.startBlock = *bs + s.rootManager.mtx.Unlock() + } + + // Create a new managed address based on the imported script. Also, + // when not a watching-only address manager, make a copy of the script + // since it will be cleared on lock and the script the caller passed + // should not be cleared out from under the caller. + scriptAddr, err := newScriptAddress( + s, ImportedAddrAccount, scriptHash, encryptedScript, + ) + if err != nil { + return nil, err + } + if !s.rootManager.WatchOnly() { + scriptAddr.scriptCT = make([]byte, len(script)) + copy(scriptAddr.scriptCT, script) + } + + // Add the new managed address to the cache of recent addresses and + // return it. + s.addrs[addrKey(scriptHash)] = scriptAddr + return scriptAddr, nil +} + +// lookupAccount loads account number stored in the manager for the given +// account name +// +// This function MUST be called with the manager lock held for reads. +func (s *ScopedKeyManager) lookupAccount(ns walletdb.ReadBucket, name string) (uint32, error) { + return fetchAccountByName(ns, &s.scope, name) +} + +// LookupAccount loads account number stored in the manager for the given +// account name +func (s *ScopedKeyManager) LookupAccount(ns walletdb.ReadBucket, name string) (uint32, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.lookupAccount(ns, name) +} + +// fetchUsed returns true if the provided address id was flagged used. +func (s *ScopedKeyManager) fetchUsed(ns walletdb.ReadBucket, + addressID []byte) bool { + + return fetchAddressUsed(ns, &s.scope, addressID) +} + +// MarkUsed updates the used flag for the provided address. +func (s *ScopedKeyManager) MarkUsed(ns walletdb.ReadWriteBucket, + address btcutil.Address) error { + + addressID := address.ScriptAddress() + err := markAddressUsed(ns, &s.scope, addressID) + if err != nil { + return maybeConvertDbError(err) + } + + // Clear caches which might have stale entries for used addresses + s.mtx.Lock() + delete(s.addrs, addrKey(addressID)) + s.mtx.Unlock() + return nil +} + +// ChainParams returns the chain parameters for this address manager. +func (s *ScopedKeyManager) ChainParams() *chaincfg.Params { + // NOTE: No need for mutex here since the net field does not change + // after the manager instance is created. + + return s.rootManager.chainParams +} + +// AccountName returns the account name for the given account number stored in +// the manager. +func (s *ScopedKeyManager) AccountName(ns walletdb.ReadBucket, account uint32) (string, error) { + return fetchAccountName(ns, &s.scope, account) +} + +// ForEachAccount calls the given function with each account stored in the +// manager, breaking early on error. +func (s *ScopedKeyManager) ForEachAccount(ns walletdb.ReadBucket, + fn func(account uint32) error) error { + + return forEachAccount(ns, &s.scope, fn) +} + +// LastAccount returns the last account stored in the manager. +func (s *ScopedKeyManager) LastAccount(ns walletdb.ReadBucket) (uint32, error) { + return fetchLastAccount(ns, &s.scope) +} + +// ForEachAccountAddress calls the given function with each address of the +// given account stored in the manager, breaking early on error. +func (s *ScopedKeyManager) ForEachAccountAddress(ns walletdb.ReadBucket, + account uint32, fn func(maddr ManagedAddress) error) error { + + s.mtx.Lock() + defer s.mtx.Unlock() + + addrFn := func(rowInterface interface{}) error { + managedAddr, err := s.rowInterfaceToManaged(ns, rowInterface) + if err != nil { + return err + } + return fn(managedAddr) + } + err := forEachAccountAddress(ns, &s.scope, account, addrFn) + if err != nil { + return maybeConvertDbError(err) + } + + return nil +} + +// ForEachActiveAccountAddress calls the given function with each active +// address of the given account stored in the manager, breaking early on error. +// +// TODO(tuxcanfly): actually return only active addresses +func (s *ScopedKeyManager) ForEachActiveAccountAddress(ns walletdb.ReadBucket, account uint32, + fn func(maddr ManagedAddress) error) error { + + return s.ForEachAccountAddress(ns, account, fn) +} + +// ForEachActiveAddress calls the given function with each active address +// stored in the manager, breaking early on error. +func (s *ScopedKeyManager) ForEachActiveAddress(ns walletdb.ReadBucket, + fn func(addr btcutil.Address) error) error { + + s.mtx.Lock() + defer s.mtx.Unlock() + + addrFn := func(rowInterface interface{}) error { + managedAddr, err := s.rowInterfaceToManaged(ns, rowInterface) + if err != nil { + return err + } + return fn(managedAddr.Address()) + } + + err := forEachActiveAddress(ns, &s.scope, addrFn) + if err != nil { + return maybeConvertDbError(err) + } + + return nil +} diff --git a/waddrmgr/sync.go b/waddrmgr/sync.go index 11cb8b5..9588a8f 100644 --- a/waddrmgr/sync.go +++ b/waddrmgr/sync.go @@ -11,8 +11,8 @@ import ( "github.com/roasbeef/btcwallet/walletdb" ) -// BlockStamp defines a block (by height and a unique hash) and is -// used to mark a point in the blockchain that an address manager element is +// BlockStamp defines a block (by height and a unique hash) and is used to mark +// a point in the blockchain that an address manager element is // synced to. type BlockStamp struct { Height int32 @@ -24,8 +24,8 @@ type BlockStamp struct { // seen blocks as height, as well as the start and current sync block stamps. type syncState struct { // startBlock is the first block that can be safely used to start a - // rescan. It is either the block the manager was created with, or - // the earliest block provided with imported addresses or scripts. + // rescan. It is either the block the manager was created with, or the + // earliest block provided with imported addresses or scripts. startBlock BlockStamp // syncedTo is the current block the addresses in the manager are known @@ -43,8 +43,8 @@ func newSyncState(startBlock, syncedTo *BlockStamp) *syncState { } // SetSyncedTo marks the address manager to be in sync with the recently-seen -// block described by the blockstamp. When the provided blockstamp is nil, -// the oldest blockstamp of the block the manager was created at and of all +// block described by the blockstamp. When the provided blockstamp is nil, the +// oldest blockstamp of the block the manager was created at and of all // imported addresses will be used. This effectively allows the manager to be // marked as unsynced back to the oldest known point any of the addresses have // appeared in the block chain.