Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow arbitrary xpub account imports. #1471

Merged
merged 1 commit into from
Oct 22, 2019
Merged

Conversation

jrick
Copy link
Member

@jrick jrick commented Jun 3, 2019

This commit implements a new JSON-RPC method "importxpub" which
records an arbitrary HD extended pubkey to the wallet database and
allows new addresses to be derived for the account. It takes two
positional parameters: an account name and the HD extended pubkey in
string format.

Addresses of imported xpub accounts are not watched and transactions
are not saved. This may change later, but the purpose of this feature
is to allow the automatic ticket buyer to derive unique voting
addresses in sequence, avoiding address reuse.

This change contains a database upgrade and it is not possible to
downgrade a wallet database once run. Reverting to older code
requires restoring the database from backup or seed restoring.

@jrick
Copy link
Member Author

jrick commented Jun 3, 2019

As mentioned, this change contains a database upgrade to version 12. This conflicts with #1330 and I would like to merge that PR to master first, and then I will rebase over and upgrade this PR to use version 13.

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

This is causing the getaddressesbyaccount RPC to error with -4: wallet.AccountBranchAddressRange: item does not exist: account 2147483648 when called with an imported xpub account. It should return all addresses of the account returned so far. Found by @davecgh.

Copy link
Member

@matheusd matheusd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable. Testing on decrediton after importing via CLI returned the following error when generating a new address for the imported account:

2019-06-06 09:23:01.985 [ERR] GRPC: Unary method /walletrpc.WalletService/NextAddress invoked by 127.0.0.1:58648 errored: rpc error: code = NotFound desc = wallet.PubKeyForAddress: item does not exist: no address TsmDjaV6Ck1LCjUba9UvCU15is3Ev94kURP        

This is the specific call that's failing:

pubKey, err := s.wallet.PubKeyForAddress(addr)

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

Hmm that is going to need an interesting fix. It fails because it is trying to look up the public key (which it just derived internally), saved by the DB (which it is not in these imported accounts, and I don't think it's a good idea to until we start watching). If instead of Wallet.New{Internal,External}Address it called an api which returned both the xpub and child index, the RPC method handler could derive both.

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

Thinking ahead to some things we may want to do with accounts in the future (such as creating an account which derives p2sh multisig addresses from two or more other xpub accounts in sequence), returning an interface (which Address already is so we could piggy back off that) and then type asserting as a more specific interface with xpub and child accessors would be a possible way to support this without adding many new APIs. Unfortunately, there's probably code out there that expects all p2pkh addresses returned to be the dcrutil.AddressPubKeyHash concrete type.

@matheusd
Copy link
Member

matheusd commented Jun 6, 2019

Unfortunately, there's probably code out there that expects all p2pkh addresses returned to be the dcrutil.AddressPubKeyHash concrete type.

Next{Internal,Externa}Address aren't documented as returning only p2pkh keys though I guess we could implicitly assume that is the case given their names.

The solution to this specific error might be to just tweak the grpc endpoint to check for the special imported accounts and not return the public key for the moment (until the address is watched and saved in the db as you point out). Or add an "AddressKind" or something to the grpc response so that in the future we can indicate whether the returned address is from an imported account, multisig voting account, etc.

Unless the multisig-generating account will also be another fixed account (similar to the imported account for scripts), in that case the address type in the response would be implied by which account you tried to generate the address in.

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

Yes, idea with that account is that it would only be used for p2sh-multisig from other stored xpubs. This would enable the stakepool idea in #1217.

It's true that code should not be switching on the concrete type since it's not guaranteed by the API. Perhaps we should try it and see what breaks?

@matheusd
Copy link
Member

matheusd commented Jun 6, 2019

It's true that code should not be switching on the concrete type since it's not guaranteed by the API.

Heh, I'd argue that since the concrete type is not enforced (since it's the interface type that is returned) and it's not explicitly documented that only p2pkh's are returned, then it's fine to change the concrete type.

Perhaps we should try it and see what breaks?

Sure thing.

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

Have something like this in mind:

package wallet

import (
        "github.com/decred/dcrd/dcrutil"
        "github.com/decred/dcrd/hdkeychain/v2"
)

type AddressPubKeyHashV0 interface {
        dcrutil.Address
        PubKeyHash160() *[20]byte
}

type XpubPather interface {
        XpubPath() (xpub *hdkeychain.ExtendedKey, branch, child uint32)
}

type xpubAddress struct {
        *dcrutil.AddressPubKeyHash
        xpub *hdkeychain.ExtendedKey
        branch, child uint32
}

func (x *xpubAddress) PubKeyHash160() *[20]byte {
        return x.AddressPubKeyHash.Hash160()
}

func (x *xpubAddress) XpubPath() (xpub *hdkeychain.ExtendedKey, branch, child uint32) {
        return x.xpub, x.branch, x.child
}

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

@matheusd
Copy link
Member

matheusd commented Jun 6, 2019

Seems nice!

@jrick
Copy link
Member Author

jrick commented Jun 6, 2019

See #1474.

Copy link
Member

@matheusd matheusd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I know you're aware of these, leaving here for completenes' sake)

  • Address discovery requires restarting after importing the xpub and waiting for a block to be mined. We can solve this by either documenting or creating an endpoint to perform a rescan+address discovery (either completely or only of specific accounts).

  • Imported accounts show a balance in some situations via gRPC (annotated inline).

@@ -682,6 +683,11 @@ func (s *Server) getBalance(ctx context.Context, icmd interface{}) (interface{},
result.Balances = make([]types.GetAccountBalanceResult, balancesLen)

for _, bal := range balances {
// Transactions are not tracked for imported xpub accounts.
if bal.Account > udb.ImportedAddrAccount {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check needs to be added to grpc as well if this is going to be done at this level of abstraction (rpc endpoints).

Another alternative would be to add this directly in the address manager when calculating the balances so it's consistent over all current (and future) comm endpoints.

Comment on lines +549 to +559
pk, err := childKey.ECPubKey()
if err != nil {
return nil, errors.E(op, err)
}
pkh := dcrutil.Hash160(pk.Serialize())
apkh, err := dcrutil.NewAddressPubKeyHash(pkh, w.chainParams,
dcrec.STEcdsaSecp256k1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can simplify to apkh, err := compat.HD2Address(childKey, w.chainParams)

@@ -576,7 +576,7 @@ func (m *Manager) AccountExtendedPubKey(dbtx walletdb.ReadTx, account uint32) (*
// unlocked for this operation to complete.
func (m *Manager) AccountExtendedPrivKey(dbtx walletdb.ReadTx, account uint32) (*hdkeychain.ExtendedKey, error) {
if account == ImportedAddrAccount {
return nil, errors.E(errors.Invalid, "imported account has no extended pubkey")
return nil, errors.E(errors.Invalid, "imported address account has no extended pubkey")
Copy link
Member

@itswisdomagain itswisdomagain Oct 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, errors.E(errors.Invalid, "imported address account has no extended pubkey")
return nil, errors.E(errors.Invalid, "imported address account has no extended privkey")

return err
}

// There may not be an account by the same name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// There may not be an account by the same name
// There may already be an account by the same name

return err
}
account++
if account < MaxAccountNum {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check should be unnecessary as fetchLastImportedAccount is guaranteed to return account >= ImportedAddrAccount and ImportedAddrAccount > MaxAccountNum

// We have the encrypted account extended keys, so save them to the
// database
row := bip0044AccountInfo(acctPubEnc, nil, 0, 0,
^uint32(0), ^uint32(0), ^uint32(0), ^uint32(0), name, DBVersion)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

importedXpubAccountVersion instead of DBVersion?

This commit implements a new JSON-RPC method "importxpub" which
records an arbitrary HD extended pubkey to the wallet database and
allows new addresses to be derived for the account.  It takes two
positional parameters: an account name and the HD extended pubkey in
string format.

This change contains a database upgrade and it is not possible to
downgrade a wallet database once run.  Reverting to older code
requires restoring the database from backup or seed restoring.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants