Merge pull request #7565 from Torakushi/accounts

Fix non-deterministic behaviour for custom accounts
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2023-06-14 23:28:42 -05:00
commit 453fbb3358
6 files changed
+157 -24

No files matched your search

+3
View File
@@ -1447,6 +1447,9 @@ var importAccountCommand = cli.Command{
The address type can usually be inferred from the key's version, but may The address type can usually be inferred from the key's version, but may
be required for certain keys to map them into the proper scope. be required for certain keys to map them into the proper scope.
If an account with the same name already exists (even with a different
key scope), an error will be returned.
For BIP-0044 keys, an address type must be specified as we intend to not For BIP-0044 keys, an address type must be specified as we intend to not
support importing BIP-0044 keys into the wallet using the legacy support importing BIP-0044 keys into the wallet using the legacy
pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will
@@ -80,6 +80,13 @@ package](https://github.com/lightningnetwork/lnd/pull/7356)
* [Fixed a bug where we didn't check for correct networks when submitting * [Fixed a bug where we didn't check for correct networks when submitting
onchain transactions](https://github.com/lightningnetwork/lnd/pull/6448). onchain transactions](https://github.com/lightningnetwork/lnd/pull/6448).
* [Fix non-deterministic behaviour in RPC calls for
custom accounts](https://github.com/lightningnetwork/lnd/pull/7565).
In theory, it should be only one custom account with a given name. However,
due to a lack of check, users could have created custom accounts with various
key scopes. The non-deterministic behaviours linked to this case are fixed,
and users can no longer create two custom accounts with the same name.
## Misc ## Misc
* [Generate default macaroons * [Generate default macaroons
@@ -157,6 +164,7 @@ unlock or create.
* Michael Street * Michael Street
* MG-ng * MG-ng
* Oliver Gugger * Oliver Gugger
* Pierre Beugnet
* Satarupa Deb * Satarupa Deb
* Shaurya Arora * Shaurya Arora
* Torkel Rogstad * Torkel Rogstad
+33
View File
@@ -2,6 +2,7 @@ package itest
import ( import (
"bytes" "bytes"
"fmt"
"testing" "testing"
"time" "time"
@@ -468,6 +469,10 @@ func testWalletImportAccount(ht *lntest.HarnessTest) {
name string name string
addrType walletrpc.AddressType addrType walletrpc.AddressType
}{ }{
{
name: "standard BIP-0044",
addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH,
},
{ {
name: "standard BIP-0049", name: "standard BIP-0049",
addrType: walletrpc. addrType: walletrpc.
@@ -550,6 +555,34 @@ func runWalletImportAccountScenario(ht *lntest.HarnessTest,
} }
dave.RPC.ImportAccount(importReq) dave.RPC.ImportAccount(importReq)
// Try to import an account with the same name but with a different
// key scope. It should return an error.
otherAddrType := walletrpc.AddressType_TAPROOT_PUBKEY
if addrType == walletrpc.AddressType_TAPROOT_PUBKEY {
otherAddrType--
}
listReq = &walletrpc.ListAccountsRequest{
Name: "default",
AddressType: otherAddrType,
}
listResp = carol.RPC.ListAccounts(listReq)
require.Len(ht, listResp.Accounts, 1)
carolAccountOtherAddrType := listResp.Accounts[0]
errAccountExists := fmt.Sprintf(
"account '%s' already exists", importedAccount,
)
importReq = &walletrpc.ImportAccountRequest{
Name: importedAccount,
ExtendedPublicKey: carolAccountOtherAddrType.ExtendedPublicKey,
AddressType: otherAddrType,
}
err := dave.RPC.ImportAccountAssertErr(importReq)
require.ErrorContains(ht, err, errAccountExists)
// We'll generate an address for Carol from Dave's node to receive some // We'll generate an address for Carol from Dave's node to receive some
// funds. // funds.
externalAddr := newExternalAddr( externalAddr := newExternalAddr(
+14
View File
@@ -235,6 +235,20 @@ func (h *HarnessRPC) ImportAccount(
return resp return resp
} }
// ImportAccountAssertErr makes the ImportAccount RPC call and asserts an error
// is returned. It then returns the error.
func (h *HarnessRPC) ImportAccountAssertErr(
req *walletrpc.ImportAccountRequest) error {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
_, err := h.WalletKit.ImportAccount(ctxt, req)
require.Error(h, err)
return err
}
// ImportPublicKey makes a RPC call to the node's WalletKitClient and asserts. // ImportPublicKey makes a RPC call to the node's WalletKitClient and asserts.
// //
//nolint:lll //nolint:lll
+64 -22
View File
@@ -82,11 +82,6 @@ var (
// requested for the default imported account within the wallet. // requested for the default imported account within the wallet.
errNoImportedAddrGen = errors.New("addresses cannot be generated for " + errNoImportedAddrGen = errors.New("addresses cannot be generated for " +
"the default imported account") "the default imported account")
// errIncompatibleAccountAddr is an error returned when the type of a
// new address being requested is incompatible with the account.
errIncompatibleAccountAddr = errors.New("incompatible address type " +
"for account")
) )
// BtcWallet is an implementation of the lnwallet.WalletController interface // BtcWallet is an implementation of the lnwallet.WalletController interface
@@ -516,18 +511,14 @@ func (b *BtcWallet) keyScopeForAccountAddr(accountName string,
return addrKeyScope, defaultAccount, nil return addrKeyScope, defaultAccount, nil
} }
// Otherwise, look up the account's key scope and check that it supports // Otherwise, look up the custom account and if it supports the given
// the requested address type. // key scope.
keyScope, account, err := b.wallet.LookupAccount(accountName) accountNumber, err := b.wallet.AccountNumber(addrKeyScope, accountName)
if err != nil { if err != nil {
return waddrmgr.KeyScope{}, 0, err return waddrmgr.KeyScope{}, 0, err
} }
if keyScope != addrKeyScope { return addrKeyScope, accountNumber, nil
return waddrmgr.KeyScope{}, 0, errIncompatibleAccountAddr
}
return keyScope, account, nil
} }
// NewAddress returns the next external or internal address for the wallet // NewAddress returns the next external or internal address for the wallet
@@ -636,17 +627,36 @@ func (b *BtcWallet) ListAccounts(name string,
break break
} }
// Otherwise, we'll retrieve the single account that's mapped by // In theory, there should be only one custom account for the
// the given name. // given name. However, due to a lack of check, users could
scope, acctNum, err := b.wallet.LookupAccount(name) // create custom accounts with various key scopes. This
if err != nil { // behaviour has been fixed but, we return all potential custom
return nil, err // accounts with the given name.
for _, scope := range waddrmgr.DefaultKeyScopes {
a, err := b.wallet.AccountPropertiesByName(
scope, name,
)
switch {
case waddrmgr.IsError(err, waddrmgr.ErrAccountNotFound):
continue
// In the specific case of a wallet initialized only by
// importing account xpubs (watch only wallets), it is
// possible that some keyscopes will be 'unknown' by the
// wallet (depending on the xpubs given to initialize
// it). If the keyscope is not found, just skip it.
case waddrmgr.IsError(err, waddrmgr.ErrScopeNotFound):
continue
case err != nil:
return nil, err
}
res = append(res, a)
} }
account, err := b.wallet.AccountProperties(scope, acctNum) if len(res) == 0 {
if err != nil { return nil, newAccountNotFoundError(name)
return nil, err
} }
res = append(res, account)
// Only the key scope filter was provided, so we'll return all accounts // Only the key scope filter was provided, so we'll return all accounts
// matching it. // matching it.
@@ -690,6 +700,18 @@ func (b *BtcWallet) ListAccounts(name string,
return res, nil return res, nil
} }
// newAccountNotFoundError returns an error indicating that the manager didn't
// find the specific account. This error is used to be compatible with the old
// 'LookupAccount' behaviour previously used.
func newAccountNotFoundError(name string) error {
str := fmt.Sprintf("account name '%s' not found", name)
return waddrmgr.ManagerError{
ErrorCode: waddrmgr.ErrAccountNotFound,
Description: str,
}
}
// RequiredReserve returns the minimum amount of satoshis that should be // RequiredReserve returns the minimum amount of satoshis that should be
// kept in the wallet in order to fee bump anchor channels if necessary. // kept in the wallet in order to fee bump anchor channels if necessary.
// The value scales with the number of public anchor channels but is // The value scales with the number of public anchor channels but is
@@ -821,6 +843,10 @@ func (b *BtcWallet) ListAddresses(name string,
// The address type can usually be inferred from the key's version, but may be // The address type can usually be inferred from the key's version, but may be
// required for certain keys to map them into the proper scope. // required for certain keys to map them into the proper scope.
// //
// For custom accounts, we will first check if there is no account with the same
// name (even with a different key scope). No custom account should have various
// key scopes as it will result in non-deterministic behaviour.
//
// For BIP-0044 keys, an address type must be specified as we intend to not // For BIP-0044 keys, an address type must be specified as we intend to not
// support importing BIP-0044 keys into the wallet using the legacy // support importing BIP-0044 keys into the wallet using the legacy
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force // pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force
@@ -838,6 +864,22 @@ func (b *BtcWallet) ImportAccount(name string, accountPubKey *hdkeychain.Extende
dryRun bool) (*waddrmgr.AccountProperties, []btcutil.Address, dryRun bool) (*waddrmgr.AccountProperties, []btcutil.Address,
[]btcutil.Address, error) { []btcutil.Address, error) {
// For custom accounts, we first check if there is no existing account
// with the same name.
if name != lnwallet.DefaultAccountName &&
name != waddrmgr.ImportedAddrAccountName {
_, err := b.ListAccounts(name, nil)
if err == nil {
return nil, nil, nil,
fmt.Errorf("account '%s' already exists",
name)
}
if !waddrmgr.IsError(err, waddrmgr.ErrAccountNotFound) {
return nil, nil, nil, err
}
}
if !dryRun { if !dryRun {
accountProps, err := b.wallet.ImportAccount( accountProps, err := b.wallet.ImportAccount(
name, accountPubKey, masterKeyFingerprint, addrType, name, accountPubKey, masterKeyFingerprint, addrType,
+35 -2
View File
@@ -97,7 +97,7 @@ func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32,
"custom change type for custom accounts") "custom change type for custom accounts")
} }
scope, account, err := b.wallet.LookupAccount(accountName) scope, account, err := b.lookupFirstCustomAccount(accountName)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -512,7 +512,7 @@ func (b *BtcWallet) FinalizePsbt(packet *psbt.Packet, accountName string) error
// number to determine if the inputs belonging to this account should be // number to determine if the inputs belonging to this account should be
// signed. // signed.
default: default:
scope, account, err := b.wallet.LookupAccount(accountName) scope, account, err := b.lookupFirstCustomAccount(accountName)
if err != nil { if err != nil {
return err return err
} }
@@ -522,3 +522,36 @@ func (b *BtcWallet) FinalizePsbt(packet *psbt.Packet, accountName string) error
return b.wallet.FinalizePsbt(keyScope, accountNum, packet) return b.wallet.FinalizePsbt(keyScope, accountNum, packet)
} }
// lookupFirstCustomAccount returns the first custom account found. In theory,
// there should be only one custom account for the given name. However, due to a
// lack of check, users could have created custom accounts with various key
// scopes. This behaviour has been fixed but, we still need to handle this
// specific case to avoid non-deterministic behaviour implied by LookupAccount.
func (b *BtcWallet) lookupFirstCustomAccount(
name string) (waddrmgr.KeyScope, uint32, error) {
var (
account *waddrmgr.AccountProperties
keyScope waddrmgr.KeyScope
)
for _, scope := range waddrmgr.DefaultKeyScopes {
var err error
account, err = b.wallet.AccountPropertiesByName(scope, name)
if waddrmgr.IsError(err, waddrmgr.ErrAccountNotFound) {
continue
}
if err != nil {
return keyScope, 0, err
}
keyScope = scope
break
}
if account == nil {
return waddrmgr.KeyScope{}, 0, newAccountNotFoundError(name)
}
return keyScope, account.AccountNumber, nil
}