Bitcoin Forum
February 28, 2025, 01:38:31 AM *
News: Community Awards voting is open
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: Deriving p2wpkh addresses (bc1) using Xpriv (dumpprivkey no-legacy fail)  (Read 177 times)
mcdouglasx (OP)
Full Member
***
Offline Offline

Activity: 560
Merit: 189



View Profile WWW
December 31, 2024, 04:30:17 PM
Merited by nc50lc (1)
 #1

Sometimes we want to backup our private keys from Bitcoin Core using the dumpprivkey command, but it fails because it only supports legacy wallets and does not support descriptor wallets. The solution to this is as follows:

From the internal Bitcoin Core console, extract your xpriv with the following command:

Code:
listdescriptors true

Once we have our xpriv, we paste it into the following Python code and choose the number of addresses to derive:

Code:
import hashlib
import bip32utils

def sha256(data):
    return hashlib.sha256(data).digest()

def ripemd160(data):
    h = hashlib.new('ripemd160')
    h.update(data)
    return h.digest()

def bech32_polymod(values):
    generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
    chk = 1
    for v in values:
        b = (chk >> 25)
        chk = (chk & 0x1ffffff) << 5 ^ v
        for i in range(5):
            chk ^= generator[i] if ((b >> i) & 1) else 0
    return chk

def bech_expand(hrp):
    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]

def bech_checksum(hrp, data):
    values = bech_expand(hrp) + data
    polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
    return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]

def bech32_encode(hrp, data):
    combined = data + bech_checksum(hrp, data)
    BECH_CHRS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
    return hrp + '1' + ''.join([BECH_CHRS[d] for d in combined])

def cbits(data, frombits, tobits, pad=True):
    acc = 0
    bits = 0
    ret = []
    maxv = (1 << tobits) - 1
    for value in data:
        if value < 0 or (value >> frombits):
            raise ValueError("Invalid value")
        acc = (acc << frombits) | value
        bits += frombits
        while bits >= tobits:
            bits -= tobits
            ret.append((acc >> bits) & maxv)
    if pad:
        if bits:
            ret.append((acc << (tobits - bits)) & maxv)
    elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
        raise ValueError("Invalid bits")
    return ret

def bech_address(pubkey):
    sha256_r = sha256(pubkey)
    ripemdr = ripemd160(sha256_r)
    data = cbits(ripemdr, 8, 5)
    data = [0] + data
    hrp = 'bc'
    address = bech32_encode(hrp, data)
    return address


xprv = "Xprv-HERE"

derive = 20 #This sets the number of child addresses to derive


bip32_root_key_obj = bip32utils.BIP32Key.fromExtendedKey(xprv)

with open("derived_addresses.txt", "w") as file:
    for i in range(derive):
        derived_key = bip32_root_key_obj.ChildKey(84 + 0x80000000).ChildKey(0 + 0x80000000).ChildKey(0 + 0x80000000).ChildKey(0).ChildKey(i)
        priv_key = derived_key.WalletImportFormat()
        pubkey = derived_key.PublicKey().hex()
        pubkey_bytes = bytes.fromhex(pubkey)
        bech32_address = bech_address(pubkey_bytes)
        file.write(f"Private Key: {priv_key}\nPublic Key: {pubkey}\nSegWit (Bech32) Address: {bech32_address}\n\n")

A text file will be created with our private keys in WIF format.


cctv5go
Newbie
*
Offline Offline

Activity: 30
Merit: 0


View Profile
December 31, 2024, 09:49:27 PM
 #2

How to import an existing private key or address into the core wallet?
mcdouglasx (OP)
Full Member
***
Offline Offline

Activity: 560
Merit: 189



View Profile WWW
December 31, 2024, 11:25:08 PM
 #3

How to import an existing private key or address into the core wallet?

This post might help you:

https://bitcoin.stackexchange.com/questions/113846/how-can-a-private-key-be-imported-to-a-descriptor-wallet

Forsyth Jones
Legendary
*
Offline Offline

Activity: 1456
Merit: 1172


Top-tier crypto casino and sportsbook


View Profile WWW
January 11, 2025, 08:04:19 PM
Merited by mcdouglasx (1)
 #4

From what I've tested, this script dumps private keys for bech32 addresses, only receiving addresses.

To dump change addresses and other scripts addresses (legacy, p2sh-segwit and bech32m) you should add more functions to this script as I described.

I have a similar script, you can base it on it, at the moment it only accepts mnemonics and bip39 passphrases, but you can make changes to allow for the inclusion of extended private keys.

Code:
import sys
import hashlib
from bip_utils import (
    Bip39MnemonicGenerator, Bip39MnemonicValidator, Bip39SeedGenerator,
    Bip44, Bip49, Bip84, Bip86, Bip44Coins, Bip49Coins, Bip84Coins, Bip86Coins,
    Bip44Changes, Bip32Slip10Secp256k1, Secp256k1PrivateKey, WifEncoder
)
from getpass import getpass

def main():
    # Ask if a new mnemonic should be created
    mnemonic_choice = input("Enter the number of words to create a new mnemonic (12, 18, 24) or press Enter to use an existing mnemonic: ").strip()

    if mnemonic_choice in ['12', '18', '24']:
        # Generate a new mnemonic with the specified number of words
        num_words = int(mnemonic_choice)
        mnemonic = Bip39MnemonicGenerator().FromWordsNumber(num_words)
        print(f"New mnemonic generated ({num_words} words): {mnemonic}")
    else:
        # Ask for the mnemonic
        mnemonic = input("Enter the mnemonic: ").strip()

    # Validate the mnemonic
    if not Bip39MnemonicValidator().IsValid(mnemonic):
        print("Invalid mnemonic!")
        sys.exit(1)

    # Ask for the passphrase (optional)
    passphrase = getpass("Enter the passphrase (optional): ").strip()

    # Generate the BIP39 seed
    seed = Bip39SeedGenerator(mnemonic).Generate(passphrase)

    # Define default paths
    deriv_paths = {
        '1': "m/44'/0'",    # p2pkh
        '2': "m/49'/0'",    # p2wpkh-p2sh
        '3': "m/84'/0'",    # p2wpkh
        '4': "m/86'/0'"     # p2tr
    }

    print("Available derivation paths and address types:")
    print("1: p2pkh (m/44'/0')")
    print("2: p2wpkh-p2sh (m/49'/0')")
    print("3: p2wpkh (m/84'/0')")
    print("4: p2tr (m/86'/0')")

    address_type = input("Choose the address type (1-4): ").strip()

    if address_type not in deriv_paths:
        print("Invalid address type!")
        sys.exit(1)

    # Base derivation path
    base_derivation_path = deriv_paths[address_type]

    # Ask for the account index
    account_index = input("Enter the account index (default 0): ").strip()
    account_index = int(account_index) if account_index.isdigit() else 0

    # Ask if generating receiving addresses (0) or change addresses (1)
    address_purpose = input("Generate receiving addresses (0) or change addresses (1)? ").strip()
    if address_purpose not in ['0', '1']:
        print("Invalid option!")
        sys.exit(1)

    # Ask from which index to display addresses
    start_index = int(input("From which index to display addresses (default 0)? ").strip() or 0)

    # Ask how many addresses to display
    address_count = int(input("How many addresses to display (default 50)? ").strip() or 50)

    # Initialize the appropriate BIP class
    bip_cls_dict = {
        '1': (Bip44, Bip44Coins.BITCOIN),
        '2': (Bip49, Bip49Coins.BITCOIN),
        '3': (Bip84, Bip84Coins.BITCOIN),
        '4': (Bip86, Bip86Coins.BITCOIN)
    }
    bip_cls, coin_type = bip_cls_dict[address_type]
    bip_obj = bip_cls.FromSeed(seed, coin_type)

    # Derive the main account
    bip_acc_obj = bip_obj.Purpose().Coin().Account(account_index)

    # Get the fingerprint of the BIP32 root public key
    root_pubkey = bip_obj.PublicKey().RawCompressed().ToBytes()
    fingerprint = hashlib.new('ripemd160', hashlib.sha256(root_pubkey).digest()).digest()[:4]

    # Derive addresses
    addresses = []
    for i in range(start_index, start_index + address_count):
        addr_obj = bip_acc_obj.Change(Bip44Changes(int(address_purpose))).AddressIndex(i)
        priv_key = addr_obj.PrivateKey().Raw().ToBytes()
        priv_key_wif = WifEncoder.Encode(Secp256k1PrivateKey.FromBytes(priv_key))
        addresses.append({
            'index': i,
            'address': addr_obj.PublicKey().ToAddress(),
            'private_key_wif': priv_key_wif,
            'hdkeypath': f"{base_derivation_path}/{account_index}'/{address_purpose}/{i}"
        })

    # Generate the BIP32 root private key
    bip32_obj = Bip32Slip10Secp256k1.FromSeed(seed)
    root_xpriv = bip32_obj.PrivateKey().ToExtended()

    # Generate the BIP32 extended private key at the account level
    account_xpriv = bip_acc_obj.PrivateKey().ToExtended()

    # Generate the WIF for the root private key
    root_priv_key_bytes = bip32_obj.PrivateKey().Raw().ToBytes()
    root_priv_key_wif = WifEncoder.Encode(Secp256k1PrivateKey.FromBytes(root_priv_key_bytes))

    # Display results
    print("\nMnemonic:", mnemonic)
    if passphrase:
        print("Passphrase:", passphrase)
    print("\nBIP32 Masterkey:", root_xpriv)
    print("BIP32 Extended Private Key (account Level):", account_xpriv)
    print("Derivation Path used:", f"{base_derivation_path}/{account_index}'/{address_purpose}")
    print("Wallet Fingerprint:", fingerprint.hex())
    print("BIP32 Root PrivKey (WIF):", root_priv_key_wif)
    print()

    for addr in addresses:
        print(f"{addr['private_key_wif']} # addr={addr['address']} hdkeypath={addr['hdkeypath']}")

    # Ask if a dump.txt file should be created
    save_to_file = input("Do you want to generate a dump.txt file to import into a wallet? (y/n): ").strip().lower()
    if save_to_file == 'y':
        with open('dump.txt', 'w') as f:
            f.write(f"Mnemonic: {mnemonic}\n")
            if passphrase:
                f.write(f"Passphrase: {passphrase}\n")
            f.write(f"\nBIP32 Masterkey: {root_xpriv}\n")
            f.write(f"BIP32 Extended Private Key (account Level): {account_xpriv}\n")
            f.write(f"Derivation Path used: {base_derivation_path}/{account_index}'/{address_purpose}\n")
            f.write(f"Wallet Fingerprint: {fingerprint.hex()}\n")
            f.write(f"BIP32 Root PrivKey (WIF): {root_priv_key_wif}\n\n")
            for addr in addresses:
                f.write(f"{addr['private_key_wif']} # addr={addr['address']} hdkeypath={addr['hdkeypath']}\n")
        print("dump.txt file exported successfully!")
    else:
        print("Output saved only to screen.")

if __name__ == "__main__":
    main()

How to import an existing private key or address into the core wallet?
Do you mean importing a single private key or an extended private key (xpriv)?

If you want to import single private keys from one or more addresses, I have a guide, you can access it through the link: [HOW-TO] Import privkeys into a Bitcoin Core descriptor wallet

To import all addresses from a wallet derived from an xpriv, follow this thread: How to import an xpriv to a descriptor wallet in bitcoin core?

mcdouglasx (OP)
Full Member
***
Offline Offline

Activity: 560
Merit: 189



View Profile WWW
January 11, 2025, 09:46:11 PM
 #5

From what I've tested, this script dumps private keys for bech32 addresses, only receiving addresses.

To dump change addresses and other scripts addresses (legacy, p2sh-segwit and bech32m) you should add more functions to this script as I described.

I have a similar script, you can base it on it, at the moment it only accepts mnemonics and bip39 passphrases, but you can make changes to allow for the inclusion of extended private keys.


thanks for the suggestions, i know this topic can be a pain for many users, the existing tools are discontinued, and are not friendly for the average non-programmer, so in my spare time i'm making an easy to use open source software to:
  • derive address from xpriv
  • derive address from wallet.dat(descriptor, sqlite)
  • extract address from wallet.dat(legacy, berkeleydb)(old)
  • fix wallet.dat (corrupt) both descriptor and legacy.

maybe next month i'll finish it (if my streak of bad luck ends)


tiffy
Jr. Member
*
Offline Offline

Activity: 58
Merit: 52


View Profile
January 12, 2025, 04:17:27 PM
Merited by mcdouglasx (1)
 #6

Sometimes we want to backup our private keys from Bitcoin Core using the dumpprivkey command, but it fails because it only supports legacy wallets and does not support descriptor wallets. The solution to this is as follows:

Thank you for posting this here. I think there is a lack of software that calculates keys independently of the core client. There is the Ian Coleman tool. See:

https://github.com/iancoleman/bip39

But the tool does not support taproot, for example. It would be fantastic if there was an easy-to-use tool for this too.

I absolutely understand that you want to check the calculations twice. But let's assume we trust the core client and the math.

Why don't you simply save the output of

Code:
listdescriptors true

I have already used it to completely restore wallets (as a test). It can also be encrypted. Linux command line:
Code:
# Export your xprv descriptors:
bitcoin-cli -rpcwallet="yourWallet" listdescriptors true > listdescriptors_true.txt
# This compresses the redundant text file by a factor of about 6:
gzip listdescriptors_true.txt
# -> listdescriptors_true.txt.gz
# Now we encrypt the compressed file:
openssl aes-256-cbc -a -salt -pbkdf2 -in listdescriptors_true.txt.gz -out listdescriptors_true.txt.gz.enc
# Enter passphrase twice
# Done! Safe listdescriptors_true.txt.gz.enc wherever you like.
And it fits easily into a QR code.
Code:
# Now let's build the QR code:
base64 listdescriptors_true.txt.gz.enc > listdescriptors_true.txt.gz.enc.b64
qrencode -s5 -r listdescriptors_true.txt.gz.enc.b64 -o listdescriptors_true.txt.gz.enc.b64.png
Output QR code on screen:
display listdescriptors_true.txt.gz.enc.b64.png
You can of course also print out the QR code.

That's just an idea. And quite independently of that, I find your project very useful.


mcdouglasx (OP)
Full Member
***
Offline Offline

Activity: 560
Merit: 189



View Profile WWW
January 12, 2025, 04:55:56 PM
 #7

Sometimes we want to backup our private keys from Bitcoin Core using the dumpprivkey command, but it fails because it only supports legacy wallets and does not support descriptor wallets. The solution to this is as follows:

Thank you for posting this here. I think there is a lack of software that calculates keys independently of the core client. There is the Ian Coleman tool. See:

https://github.com/iancoleman/bip39

But the tool does not support taproot, for example. It would be fantastic if there was an easy-to-use tool for this too.

I absolutely understand that you want to check the calculations twice. But let's assume we trust the core client and the math.

Why don't you simply save the output of

Code:
listdescriptors true

I have already used it to completely restore wallets (as a test). It can also be encrypted. Linux command line:
Code:
# Export your xprv descriptors:
bitcoin-cli -rpcwallet="yourWallet" listdescriptors true > listdescriptors_true.txt
# This compresses the redundant text file by a factor of about 6:
gzip listdescriptors_true.txt
# -> listdescriptors_true.txt.gz
# Now we encrypt the compressed file:
openssl aes-256-cbc -a -salt -pbkdf2 -in listdescriptors_true.txt.gz -out listdescriptors_true.txt.gz.enc
# Enter passphrase twice
# Done! Safe listdescriptors_true.txt.gz.enc wherever you like.
And it fits easily into a QR code.
Code:
# Now let's build the QR code:
base64 listdescriptors_true.txt.gz.enc > listdescriptors_true.txt.gz.enc.b64
qrencode -s5 -r listdescriptors_true.txt.gz.enc.b64 -o listdescriptors_true.txt.gz.enc.b64.png
Output QR code on screen:
display listdescriptors_true.txt.gz.enc.b64.png
You can of course also print out the QR code.

That's just an idea. And quite independently of that, I find your project very useful.




Thank you,  I'm doing this with users with little computer knowledge in mind. There are people who don't even know that the console exists.
If we want a larger adoption of Bitcoin, we need to create user-friendly tools.

Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!