waddrmgr: store watch-only accounts under new account type

Watch-only accounts are usually backed by an external signer as they do
not contain any private key information. Some external signers require a
root key fingerprint for identification and signing purposes. In order
to guarantee compatibility with external signers, we need to persist the
root key fingerprint within the database.

Before this change, watch-only accounts used the default account
database structure. In this commit, we introduce a new account type to
store different information for watch-only accounts only. This isn't a
breaking change as watch-only accounts have yet to be supported by the
primary user of the wallet (lnd). With this new account type, we can
avoid the empty private key fields, which are irrelevant to watch-only
accounts, and we can store the root key fingerprint.
This commit is contained in:
Wilmer Paulino 2021-02-16 17:01:15 -08:00
parent 0492cb4507
commit 198b0b8dae
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F
3 changed files with 377 additions and 99 deletions

View file

@ -6,6 +6,7 @@
package waddrmgr package waddrmgr
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"errors" "errors"
@ -83,6 +84,12 @@ const (
// database. This is an account that re-uses the key derivation schema // database. This is an account that re-uses the key derivation schema
// of BIP0044-like accounts. // of BIP0044-like accounts.
accountDefault accountType = 0 // not iota as they need to be stable accountDefault accountType = 0 // not iota as they need to be stable
// accountWatchOnly is the account type used for storing watch-only
// accounts within the database. This is an account that re-uses the key
// derivation schema of BIP0044-like accounts and does not store private
// keys.
accountWatchOnly accountType = 1
) )
// dbAccountRow houses information stored about an account in the database. // dbAccountRow houses information stored about an account in the database.
@ -102,6 +109,18 @@ type dbDefaultAccountRow struct {
name string name string
} }
// dbWatchOnlyAccountRow houses additional information stored about a watch-only
// account in the databse.
type dbWatchOnlyAccountRow struct {
dbAccountRow
pubKeyEncrypted []byte
masterKeyFingerprint uint32
nextExternalIndex uint32
nextInternalIndex uint32
name string
addrSchema *ScopeAddrSchema
}
// dbAddressRow houses common information stored about an address in the // dbAddressRow houses common information stored about an address in the
// database. // database.
type dbAddressRow struct { type dbAddressRow struct {
@ -809,6 +828,159 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte,
return rawData return rawData
} }
// deserializeWatchOnlyAccountRow deserializes the raw data from the passed
// account row as a watch-only account.
func deserializeWatchOnlyAccountRow(accountID []byte,
row *dbAccountRow) (*dbWatchOnlyAccountRow, error) {
// The serialized BIP0044 watch-only account raw data format is:
// <encpubkeylen><encpubkey><masterkeyfingerprint><nextextidx>
// <nextintidx><namelen><name>
//
// 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key
// fingerprint + 4 bytes next external index + 4 bytes next internal
// index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes
// addr schema (if exists)
// Given the above, the length of the entry must be at a minimum
// the constant value sizes.
if len(row.rawData) < 21 {
str := fmt.Sprintf("malformed serialized watch-only account "+
"for key %x", accountID)
return nil, managerError(ErrDatabase, str, nil)
}
retRow := dbWatchOnlyAccountRow{
dbAccountRow: *row,
}
r := bytes.NewReader(row.rawData)
var pubLen uint32
err := binary.Read(r, binary.LittleEndian, &pubLen)
if err != nil {
return nil, err
}
retRow.pubKeyEncrypted = make([]byte, pubLen)
err = binary.Read(r, binary.LittleEndian, &retRow.pubKeyEncrypted)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.LittleEndian, &retRow.masterKeyFingerprint)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.LittleEndian, &retRow.nextExternalIndex)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.LittleEndian, &retRow.nextInternalIndex)
if err != nil {
return nil, err
}
var nameLen uint32
err = binary.Read(r, binary.LittleEndian, &nameLen)
if err != nil {
return nil, err
}
name := make([]byte, nameLen)
err = binary.Read(r, binary.LittleEndian, &name)
if err != nil {
return nil, err
}
retRow.name = string(name)
var addrSchemaExists bool
err = binary.Read(r, binary.LittleEndian, &addrSchemaExists)
if err != nil {
return nil, err
}
if addrSchemaExists {
var addrSchemaBytes [2]byte
err = binary.Read(r, binary.LittleEndian, &addrSchemaBytes)
if err != nil {
return nil, err
}
retRow.addrSchema = scopeSchemaFromBytes(addrSchemaBytes[:])
}
return &retRow, nil
}
// serializeWatchOnlyAccountRow returns the serialization of the raw data field
// for a watch-only account.
func serializeWatchOnlyAccountRow(encryptedPubKey []byte, masterKeyFingerprint,
nextExternalIndex, nextInternalIndex uint32, name string,
addrSchema *ScopeAddrSchema) ([]byte, error) {
// The serialized BIP0044 account raw data format is:
// <encpubkeylen><encpubkey><masterkeyfingerprint><nextextidx>
// <nextintidx><namelen><name>
//
// 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key
// fingerprint + 4 bytes next external index + 4 bytes next internal
// index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes
// addr schema (if exists)
pubLen := uint32(len(encryptedPubKey))
nameLen := uint32(len(name))
addrSchemaExists := addrSchema != nil
var addrSchemaBytes []byte
if addrSchemaExists {
addrSchemaBytes = scopeSchemaToBytes(addrSchema)
}
bufLen := 21 + pubLen + nameLen + uint32(len(addrSchemaBytes))
buf := bytes.NewBuffer(make([]byte, 0, bufLen))
err := binary.Write(buf, binary.LittleEndian, pubLen)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, encryptedPubKey)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, masterKeyFingerprint)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, nextExternalIndex)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, nextInternalIndex)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, nameLen)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, []byte(name))
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, addrSchemaExists)
if err != nil {
return nil, err
}
if addrSchemaExists {
err = binary.Write(buf, binary.LittleEndian, addrSchemaBytes)
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// forEachKeyScope calls the given function for each known manager scope // forEachKeyScope calls the given function for each known manager scope
// within the set of scopes known by the root manager. // within the set of scopes known by the root manager.
func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error { func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error {
@ -947,6 +1119,8 @@ func fetchAccountInfo(ns walletdb.ReadBucket, scope *KeyScope,
switch row.acctType { switch row.acctType {
case accountDefault: case accountDefault:
return deserializeDefaultAccountRow(accountID, row) return deserializeDefaultAccountRow(accountID, row)
case accountWatchOnly:
return deserializeWatchOnlyAccountRow(accountID, row)
} }
str := fmt.Sprintf("unsupported account type '%d'", row.acctType) str := fmt.Sprintf("unsupported account type '%d'", row.acctType)
@ -1087,8 +1261,9 @@ func putAccountRow(ns walletdb.ReadWriteBucket, scope *KeyScope,
return nil return nil
} }
// putAccountInfo stores the provided account information to the database. // putDefaultAccountInfo stores the provided default account information to the
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, // database.
func putDefaultAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
account uint32, encryptedPubKey, encryptedPrivKey []byte, account uint32, encryptedPubKey, encryptedPrivKey []byte,
nextExternalIndex, nextInternalIndex uint32, name string) error { nextExternalIndex, nextInternalIndex uint32, name string) error {
@ -1103,7 +1278,38 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
acctType: accountDefault, acctType: accountDefault,
rawData: rawData, rawData: rawData,
} }
if err := putAccountRow(ns, scope, account, &acctRow); err != nil { return putAccountInfo(ns, scope, account, &acctRow, name)
}
// putWatchOnlyAccountInfo stores the provided watch-only account information to
// the database.
func putWatchOnlyAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
account uint32, encryptedPubKey []byte, masterKeyFingerprint,
nextExternalIndex, nextInternalIndex uint32, name string,
addrSchema *ScopeAddrSchema) error {
rawData, err := serializeWatchOnlyAccountRow(
encryptedPubKey, masterKeyFingerprint, nextExternalIndex,
nextInternalIndex, name, addrSchema,
)
if err != nil {
return err
}
// TODO(roasbeef): pass scope bucket directly??
acctRow := dbAccountRow{
acctType: accountWatchOnly,
rawData: rawData,
}
return putAccountInfo(ns, scope, account, &acctRow, name)
}
// putAccountInfo stores the provided account information to the database.
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
account uint32, acctRow *dbAccountRow, name string) error {
if err := putAccountRow(ns, scope, account, acctRow); err != nil {
return err return err
} }
@ -1113,11 +1319,7 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
} }
// Update account name index. // Update account name index.
if err := putAccountNameIndex(ns, scope, account, name); err != nil { return putAccountNameIndex(ns, scope, account, name)
return err
}
return nil
} }
// putLastAccount stores the provided metadata - last account - to the // putLastAccount stores the provided metadata - last account - to the
@ -1479,13 +1681,16 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope,
if err != nil { if err != nil {
return err return err
} }
switch row.acctType {
case accountDefault:
arow, err := deserializeDefaultAccountRow(accountID, row) arow, err := deserializeDefaultAccountRow(accountID, row)
if err != nil { if err != nil {
return err return err
} }
// Increment the appropriate next index depending on whether the branch // Increment the appropriate next index depending on whether the
// is internal or external. // branch is internal or external.
nextExternalIndex := arow.nextExternalIndex nextExternalIndex := arow.nextExternalIndex
nextInternalIndex := arow.nextInternalIndex nextInternalIndex := arow.nextInternalIndex
if branch == InternalBranch { if branch == InternalBranch {
@ -1496,15 +1701,44 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope,
// Reserialize the account with the updated index and store it. // Reserialize the account with the updated index and store it.
row.rawData = serializeDefaultAccountRow( row.rawData = serializeDefaultAccountRow(
arow.pubKeyEncrypted, arow.privKeyEncrypted, nextExternalIndex, arow.pubKeyEncrypted, arow.privKeyEncrypted,
nextInternalIndex, arow.name, nextExternalIndex, nextInternalIndex, arow.name,
) )
case accountWatchOnly:
arow, err := deserializeWatchOnlyAccountRow(accountID, row)
if err != nil {
return err
}
// Increment the appropriate next index depending on whether the
// branch is internal or external.
nextExternalIndex := arow.nextExternalIndex
nextInternalIndex := arow.nextInternalIndex
if branch == InternalBranch {
nextInternalIndex = index + 1
} else {
nextExternalIndex = index + 1
}
// Reserialize the account with the updated index and store it.
row.rawData, err = serializeWatchOnlyAccountRow(
arow.pubKeyEncrypted, arow.masterKeyFingerprint,
nextExternalIndex, nextInternalIndex, arow.name,
arow.addrSchema,
)
if err != nil {
return err
}
}
err = bucket.Put(accountID, serializeAccountRow(row)) err = bucket.Put(accountID, serializeAccountRow(row))
if err != nil { if err != nil {
str := fmt.Sprintf("failed to update next index for "+ str := fmt.Sprintf("failed to update next index for "+
"address %x, account %d", addressID, account) "address %x, account %d", addressID, account)
return managerError(ErrDatabase, str, err) return managerError(ErrDatabase, str, err)
} }
return nil return nil
} }
@ -1741,6 +1975,9 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error {
str := "failed to delete account private key" str := "failed to delete account private key"
return managerError(ErrDatabase, str, err) return managerError(ErrDatabase, str, err)
} }
// Watch-only accounts don't contain any private keys.
case accountWatchOnly:
} }
return nil return nil

View file

@ -1678,7 +1678,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
} }
// Save the information for the default account to the database. // Save the information for the default account to the database.
err = putAccountInfo( err = putDefaultAccountInfo(
ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0, ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0,
defaultAccountName, defaultAccountName,
) )
@ -1686,7 +1686,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
return err return err
} }
return putAccountInfo( return putDefaultAccountInfo(
ns, &scope, ImportedAddrAccount, nil, nil, 0, 0, ns, &scope, ImportedAddrAccount, nil, nil, 0, 0,
ImportedAddrAccountName, ImportedAddrAccountName,
) )

View file

@ -309,71 +309,95 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
return nil, maybeConvertDbError(err) return nil, maybeConvertDbError(err)
} }
// Ensure the account type is a default account. decryptKey := func(cryptoKey EncryptorDecryptor,
row, ok := rowInterface.(*dbDefaultAccountRow) encryptedKey []byte) (*hdkeychain.ExtendedKey, error) {
if !ok {
str := fmt.Sprintf("unsupported account type %T", row) serializedKey, err := cryptoKey.Decrypt(encryptedKey)
return nil, managerError(ErrDatabase, str, nil) if err != nil {
return nil, err
}
return hdkeychain.NewKeyFromString(string(serializedKey))
} }
// Use the crypto public key to decrypt the account public extended // The wallet will only contain private keys for default accounts if the
// key. // wallet's not set up as watch-only and it's been unlocked.
serializedKeyPub, err := s.rootManager.cryptoKeyPub.Decrypt(row.pubKeyEncrypted) watchOnly := s.rootManager.watchOnly()
if err != nil { hasPrivateKey := !s.rootManager.isLocked() && !watchOnly
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 // Create the new account info with the known information. The rest of
// the fields are filled out below. // the fields are filled out below.
acctInfo := &accountInfo{ var acctInfo *accountInfo
switch row := rowInterface.(type) {
case *dbDefaultAccountRow:
acctInfo = &accountInfo{
acctName: row.name, acctName: row.name,
acctKeyEncrypted: row.privKeyEncrypted, acctKeyEncrypted: row.privKeyEncrypted,
acctKeyPub: acctKeyPub,
nextExternalIndex: row.nextExternalIndex, nextExternalIndex: row.nextExternalIndex,
nextInternalIndex: row.nextInternalIndex, nextInternalIndex: row.nextInternalIndex,
} }
watchOnly := s.rootManager.watchOnly() || len(acctInfo.acctKeyEncrypted) == 0 // Use the crypto public key to decrypt the account public
private := !s.rootManager.isLocked() && !watchOnly // extended key.
if private { acctInfo.acctKeyPub, err = decryptKey(
// Use the crypto private key to decrypt the account private s.rootManager.cryptoKeyPub, row.pubKeyEncrypted,
// extended keys. )
decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted)
if err != nil { if err != nil {
str := fmt.Sprintf("failed to decrypt private key for "+ str := fmt.Sprintf("failed to decrypt public key for "+
"account %d", account) "account %d", account)
return nil, managerError(ErrCrypto, str, err) return nil, managerError(ErrCrypto, str, err)
} }
acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted)) if hasPrivateKey {
// Use the crypto private key to decrypt the account
// private extended keys.
acctInfo.acctKeyPriv, err = decryptKey(
s.rootManager.cryptoKeyPriv, row.privKeyEncrypted,
)
if err != nil { if err != nil {
str := fmt.Sprintf("failed to create extended private "+ str := fmt.Sprintf("failed to decrypt private "+
"key for account %d", account) "key for account %d", account)
return nil, managerError(ErrKeyChain, str, err) return nil, managerError(ErrCrypto, str, err)
} }
acctInfo.acctKeyPriv = acctKeyPriv }
case *dbWatchOnlyAccountRow:
acctInfo = &accountInfo{
acctName: row.name,
nextExternalIndex: row.nextExternalIndex,
nextInternalIndex: row.nextInternalIndex,
}
// Use the crypto public key to decrypt the account public
// extended key.
acctInfo.acctKeyPub, err = decryptKey(
s.rootManager.cryptoKeyPub, row.pubKeyEncrypted,
)
if err != nil {
str := fmt.Sprintf("failed to decrypt public key for "+
"account %d", account)
return nil, managerError(ErrCrypto, str, err)
}
watchOnly = true
hasPrivateKey = false
default:
str := fmt.Sprintf("unsupported account type %T", row)
return nil, managerError(ErrDatabase, str, nil)
} }
// Derive and cache the managed address for the last external address. // Derive and cache the managed address for the last external address.
branch, index := ExternalBranch, row.nextExternalIndex branch, index := ExternalBranch, acctInfo.nextExternalIndex
if index > 0 { if index > 0 {
index-- index--
} }
lastExtAddrPath := DerivationPath{ lastExtAddrPath := DerivationPath{
InternalAccount: account, InternalAccount: account,
Account: acctKeyPub.ChildIndex(), Account: acctInfo.acctKeyPub.ChildIndex(),
Branch: branch, Branch: branch,
Index: index, Index: index,
} }
lastExtKey, err := s.deriveKey(acctInfo, branch, index, private) lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -384,17 +408,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
acctInfo.lastExternalAddr = lastExtAddr acctInfo.lastExternalAddr = lastExtAddr
// Derive and cache the managed address for the last internal address. // Derive and cache the managed address for the last internal address.
branch, index = InternalBranch, row.nextInternalIndex branch, index = InternalBranch, acctInfo.nextInternalIndex
if index > 0 { if index > 0 {
index-- index--
} }
lastIntAddrPath := DerivationPath{ lastIntAddrPath := DerivationPath{
InternalAccount: account, InternalAccount: account,
Account: acctKeyPub.ChildIndex(), Account: acctInfo.acctKeyPub.ChildIndex(),
Branch: branch, Branch: branch,
Index: index, Index: index,
} }
lastIntKey, err := s.deriveKey(acctInfo, branch, index, private) lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1371,7 +1395,7 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket,
// We have the encrypted account extended keys, so save them to the // We have the encrypted account extended keys, so save them to the
// database // database
err = putAccountInfo( err = putDefaultAccountInfo(
ns, &s.scope, account, acctPubEnc, acctPrivEnc, 0, 0, name, ns, &s.scope, account, acctPubEnc, acctPrivEnc, 0, 0, name,
) )
if err != nil { if err != nil {
@ -1435,8 +1459,9 @@ func (s *ScopedKeyManager) newAccountWatchingOnly(ns walletdb.ReadWriteBucket, a
// We have the encrypted account extended keys, so save them to the // We have the encrypted account extended keys, so save them to the
// database // database
err = putAccountInfo( // TODO: set master key fingerprint and addr schema.
ns, &s.scope, account, acctPubEnc, nil, 0, 0, name, err = putWatchOnlyAccountInfo(
ns, &s.scope, account, acctPubEnc, 0, 0, 0, name, nil,
) )
if err != nil { if err != nil {
return err return err
@ -1478,23 +1503,19 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket,
return err 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. // Remove the old name key from the account id index.
if err = deleteAccountIDIndex(ns, &s.scope, account); err != nil { if err = deleteAccountIDIndex(ns, &s.scope, account); err != nil {
return err return err
} }
switch row := rowInterface.(type) {
case *dbDefaultAccountRow:
// Remove the old name key from the account name index. // Remove the old name key from the account name index.
if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil {
return err return err
} }
err = putAccountInfo(
err = putDefaultAccountInfo(
ns, &s.scope, account, row.pubKeyEncrypted, ns, &s.scope, account, row.pubKeyEncrypted,
row.privKeyEncrypted, row.nextExternalIndex, row.privKeyEncrypted, row.nextExternalIndex,
row.nextInternalIndex, name, row.nextInternalIndex, name,
@ -1503,6 +1524,26 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket,
return err return err
} }
case *dbWatchOnlyAccountRow:
// Remove the old name key from the account name index.
if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil {
return err
}
err = putWatchOnlyAccountInfo(
ns, &s.scope, account, row.pubKeyEncrypted,
row.masterKeyFingerprint, row.nextExternalIndex,
row.nextInternalIndex, name, row.addrSchema,
)
if err != nil {
return err
}
default:
str := fmt.Sprintf("unsupported account type %T", row)
return managerError(ErrDatabase, str, nil)
}
// Update in-memory account info with new name if cached and the db // Update in-memory account info with new name if cached and the db
// write was successful. // write was successful.
if err == nil { if err == nil {