meherett/python-hdwallet

Attempt to generate watch-only wallets fails

ravi-ojha opened this issue · 8 comments

Theoretically, it should be possible to generate watch-only addresses from xpublic_key.

However, the code errors when attempting something like this

bip44_hdwallet: BIP44HDWallet = BIP44HDWallet(cryptocurrency=BitcoinMainnet)
bip44_hdwallet.from_xpublic_key(xpublic_key=XPUB_KEY)

for address_index in range(10):
    bip44_derivation = BIP44Derivation(
        cryptocurrency=BitcoinMainnet, account=0, change=False, address=address_index
    )
    bip44_hdwallet.from_path(path=bip44_derivation)
    print(f"({address_index}) {bip44_hdwallet.path()} {bip44_hdwallet.address()}")
    bip44_hdwallet.clean_derivation()

The error is thrown by _derive_key_by_index function.

    437         i_str = struct.pack(">L", index)
    438         if index & BIP32KEY_HARDEN:
--> 439             data = b"\0" + self._key.to_string() + i_str
    440         else:
    441             data = unhexlify(self.public_key()) + i_str

AttributeError: 'NoneType' object has no attribute 'to_string'

Of course, in watch-only wallets, private key would be null, and it's trying to access _key which is empty as expected. Hence the failure.

@ravi-ojha The hardened derivation path does not work for xpublic key, it only works for xprivate key. But, In BIP44HDWallet the default derivation has a hardened m/44' index. So, Instead of this use your own derivation path and use HDWallet class, Sir.

For example but first upgrade it into v1.3.1

hdwallet: HDWallet = HDWallet(cryptocurrency=BitcoinMainnet)
# hdwallet.from_xpublic_key(xpublic_key=XPUB_KEY)
hdwallet.from_root_xpublic_key(xpublic_key=XPUB_KEY, strict=False)

for address_index in range(10):

    # hdwallet.from_path(path=f"m/44/{BitcoinMainnet.COIN_TYPE.INDEX}/0/0/{address_index}")

    hdwallet.from_index(44, hardened=False)
    hdwallet.from_index(BitcoinMainnet.COIN_TYPE.INDEX, hardened=False)
    hdwallet.from_index(0, hardened=False)
    hdwallet.from_index(0, hardened=False)
    hdwallet.from_index(address_index, hardened=False)

    print(f"({address_index}) {hdwallet.path()} {hdwallet.p2pkh_address()}")  # hdwallet.p2pkh_address() the same with bip44_hdwallet.address()

    hdwallet.clean_derivation()

Maybe I should've added more context and explanation in the first comment.

Let's take an example: say m/49'/0'/0' (i.e. hardened upto account going by m / purpose' / coin_type' / account' / change / address_index)

Using an xpub for path m/49'/0'/0', theoretically, we can generate all the addresses and change addresses for one account. That is to say m/49'/0'/0'/i/j where 0 <= i,j <= 2^32-1

That's because i and j are not hardened. It's not advisable to move away from the standard m/49'/0'/0' hardened up to the account in my use case.

I ended up doing this using electrum library and with the following snippet.

from electrum import bitcoin
from electrum.bip32 import BIP32Node


def get_p2wpkh_p2sh_btc_address(xpub: str, account_idx: int = 0, change: int = 0):
    bip32_node = BIP32Node.from_xkey(xpub)
    path = "{}/{}".format(change, account_idx)
    public_key_hex = bip32_node.subkey_at_public_derivation(
        path
    ).eckey.get_public_key_hex()
    return bitcoin.pubkey_to_address("p2wpkh-p2sh", public_key_hex)

Just to avoid any confusion, the xpub here is "Account Extended Public Key" from https://iancoleman.io/bip39/
We can make the function generic but that's all I needed for one-time use.

Use this example derivation m/49/0/0/i/j instead of this m/49'/0'/0'/i/j path. If any derivation path has this ' symbol (hardened), it doesn't work for xpublic key derivation, Sir.

Hey @meherett Are you sure you read my above comment? I added an example script that generates the addresses (and change addresses) using electrum wallet library with just xpublic key.

This is how watch-only wallets work.

Yes @ravi-ojha. It's not working also here https://iancoleman.io/bip39/

This is the correct way in iancoleman:

Screen Shot 2021-08-28 at 04 45 24

If you tried from the seed that you created this xpub, take it out of the example:
python-hdwallet / examples / from_root_xpublic_key.py

and drift using the path: m / 44 '/ 0' / 0 '/ 0/0 you would get the same result

from electrum import bitcoin
from electrum.bip32 import BIP32Node

could you use it for other currencies? ETH, LTC, etc
Thanks"