10.4: Exporting Secrets from Bitcoin Core
We've spent three sections importing a secret into Bitcoin Core. We're now going to reverse course and see how to export a secret. (It's a lot easier!)
Reset Your Wallet
We're going to continue working in mainnet for this example, but you should use a Bitcoin Core-generated wallet, not the descriptor-based wallet we imported in the last section.
Unload your "seed" wallet from the last section or create a new "" wallet depending on what you've done to date with Bitcoin Core.
bitcoin-cli unloadwallet "seed"
| {
| }
bitcoin-cli listwallets
| [
| ""
| ]
Understand What You Can Export
It would be ideal to export seeds or seed phrases, but Bitcoin Core doesn't currently support doing so.
That leaves you with two options:
- Export a master private key, which can be used to recreate all descriptors.
- Export individual descriptors.
The first option makes it easy to store just a tiny bit of information and restore everything from it, but you have to know what derivation paths your addresses are attached to (or else, you have to check addresses for every possible derivation path when you import).
The second option requires more data storage, but ensures that you have the precise data on the accounts (derivation paths) where you might have funds.
Understand Bitcoin Core Outputs
The listdescriptors RPC command is what you use to retrieve
information about your keys and descriptors for export. You've seen it a number
of times:
bitcoin-cli listdescriptors
| {
| "wallet_name": "",
| "descriptors": [
| {
| "desc": "pkh([3ce1d173/44h/0h/0h]xpub6BxVLeeQGSK4tVGLLZopaabNwFUZe223FfLCAa3PWNxpwqnCcZVRvDNH13fKh4Ea5w785U9a7u1LX5Tu9m9gpdi2i5PEE6aMNvXGi2LuKdV/0/*)#q8nl9w4h",
| "timestamp": 1779999396,
| "active": true,
| "internal": false,
| "range": [
| 0,
| 999
| ],
| "next": 0,
| "next_index": 0
| },
| {
| "desc": "pkh([3ce1d173/44h/0h/0h]xpub6BxVLeeQGSK4tVGLLZopaabNwFUZe223FfLCAa3PWNxpwqnCcZVRvDNH13fKh4Ea5w785U9a7u1LX5Tu9m9gpdi2i5PEE6aMNvXGi2LuKdV/1/*)#3nk7cm90",
| "timestamp": 1779999396,
| "active": true,
| "internal": true,
| "range": [
| 0,
| 999
| ],
| "next": 0,
| "next_index": 0
| },
| {
| "desc": "sh(wpkh([3ce1d173/49h/0h/0h]xpub6BopUUpLTpQDti8eEjdRKhp1bGcxHXJ9Mvdm23iSPSPh84ofH6icrDg8sH4kowsZEDcYynaHPqVS4mvrMvgHpi2sNKSBEG9s11SeR9yfvJ2/0/*))#32q0qkdf",
| "timestamp": 1779999396,
| "active": true,
| "internal": false,
| "range": [
| 0,
| 999
| ],
| "next": 0,
| "next_index": 0
| },
|
| ...
| ]
| }
By default, each descriptor shown by listdescriptors contains the
account public key for that descriptor. You'll note that they're the
same for the external and internal descriptors for the address, but
when you move on to a different derivation path, they're different.
So the public keys for the two 44h/0h/0h addresses are
xpub6BxVLe... while the public key for the 49h/0h/0h address is
xpub6BopUU.... You know that these addresses are account keys
because the account derivation path is given before the key in the
brackets.
Now, public keys aren't at all useful if you're trying to backup your
secrets. Fortunately, listdescriptors will give you private keys if
you add a true argument (which sets the private named argument to
true).
bitcoin-cli -rpcwallet="" listdescriptors true
| {
| "wallet_name": "",
| "descriptors": [
| {
| "desc": "pkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/44h/0h/0h/0/*)#e0uydrgt",
| "timestamp": 1779999396,
| "active": true,
| "internal": false,
| "range": [
| 0,
| 999
| ],
| "next": 0,
| "next_index": 0
| },
| {
| "desc": "pkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/44h/0h/0h/1/*)#gme9skcn",
| "timestamp": 1779999396,
| "active": true,
| "internal": true,
| "range": [
| 0,
| 999
| ],
| "next": 0,
| "next_index": 0
| },
| {
| "desc": "sh(wpkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/49h/0h/0h/0/*))#pfqdkhdr",
| "timestamp": 1779999396,
| "active": true,
| "internal": false,
| "range": [
| 0,
| 999
| ],
| "next": 0,
| "next_index": 0
| },
|
| ...
|
| ]
| }
You'll note that the [bracketed] prefix is now gone. Instead, the entire derivation path appears after the key (which is now a private key). That means that you're now seeing the master private key, before any derivation occurs. As you'd expect, that master key is exactly the same for each of the accounts (derivation paths).
It's great that Bitcoin Core gives you access to the master private key.
It's not great that it shows the underived master key in each of its
account descriptors, because that means that if any of them is
compromised, they all are. (But we can use keytool to change what we
store, which we'll do in the next section.)
Testing Derivation
If you're ever unsure of what Bitcoin Core (or any other Bitcoin
program) is showing you, then you can always use keytool to test
your understanding.
In this case, we think we understand that xprv9s21Zr... is a master
private key and `xpub6BxVLe... is an account public key, derived
from it for 44h/0h/0h. You can verify that by inputting the
(purported) master key into keytool and verifying that the account
public key for 44h/0h/0h is what you expect.
keytool --master-key xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4 --account-derivation-path "m/44h/0h/0h" account-pub-key-base58
| xpub6BxVLeeQGSK4tVGLLZopaabNwFUZe223FfLCAa3PWNxpwqnCcZVRvDNH13fKh4Ea5w785U9a7u1LX5Tu9m9gpdi2i5PEE6aMNvXGi2LuKdV
It is!
Store Your Data
You can now collect the information you'd need to restore this
data. jq can be used to pull out the whole list of descriptors:
DESCS=$(bitcoin-cli listdescriptors true | jq -r '.descriptors[].desc')
DESC_ARRAY=($DESCS)
for ((i = 0; i < 8; i++)); do
echo "$i: ${DESC_ARRAY[$i]}";
done
| 0: pkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/44h/0h/0h/0/*)#e0uydrgt
| 1: pkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/44h/0h/0h/1/*)#gme9skcn
| 2: sh(wpkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/49h/0h/0h/0/*))#pfqdkhdr
| 3: sh(wpkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/49h/0h/0h/1/*))#82ggd6xh
| 4: tr(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/86h/0h/0h/0/*)#q58cmp0l
| 5: tr(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/86h/0h/0h/1/*)#3qzex5l8
| 6: wpkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/84h/0h/0h/0/*)#u8jwrsql
| 7: wpkh(xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/84h/0h/0h/1/*)#dnh079s8
Since all of the descriptors include the master private key, you can retrieve it from any of them.
MP_KEY=$(echo ${DESC_ARRAY[0]} | awk -F"[()]" '{print $2}' | awk -F"/" '{print $1}')
echo $MP_KEY
| xprv9s21ZrQH143K4YGw3kaAmh3ezx1Xo4AanFmnX8TbaGghuNxXP9MDt2rPrLDyouX5kg9fiDJn7BELPnWRKwtVFn6aUBaNYJC9Mu6s39DnAo4/44h/0h/0h/0/*
Summary: Exporting Secrets from Bitcoin Core
Bitcoin Core doesn't currently allow you to export the seed. However,
you can export either your master private key or your descriptors from
the listdescriptors command
What's Next?
Conclude "Exploring the Ecosystem" with ยง10.5: Storing Secrets with Envelope.