Bitcoin Forum
November 22, 2025, 08:10:32 AM *
News: Latest Bitcoin Core release: 30.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: How do I guarantee the watch-only wallet file has no private keys?  (Read 281 times)
takuma sato (OP)
Hero Member
*****
Offline Offline

Activity: 813
Merit: 702


View Profile
October 23, 2025, 02:47:47 AM
Merited by flapduck (2), vapourminer (1), ABCbits (1)
 #1

Suppose you created a wallet.dat in 2013, then kept using Bitcoin Core until version 0.19, so you stopped using it 5 years ago. The wallet.dat is a non HD wallet, but I guess it has been updated and the wallet structure is not the same as in 2013.

So now you are using Bitcoin Knots, and you want to create a watch-only wallet for this wallet. I did this:

-On the airgap laptop, use the "migrate wallet" tool on the GUI to make the old wallet a descriptor wallet
-On the airgap laptop, run "listdescriptors false" on this updated wallet.dat file (this supposedly guarantees there are no private keys or anything that could compromise the funds)
-Copy the output
-Paste it in the online laptop wallet-dat that you created with the option to make it a watch-only wallet which disables private keys using "importdescriptors '[contentsoftheoutputoflistdescriptorsfalsehere]'"

This if nothing goes wrong, should generate a watch-only wallet where you can safely broadcast PSBT transactions generated on your offline wallet. My question is: How do you triple check there are no private keys ever touching your online node or anything that could be used as an attack vector?

Should you never for instance use raw PSBT text and stick to the .psbt files?

Any other things to check that this whole thing is running as intended?
Satofan44
Full Member
***
Offline Offline

Activity: 210
Merit: 572


Don't hold me responsible for your shortcomings.


View Profile
October 23, 2025, 07:00:05 PM
Merited by ABCbits (3), flapduck (2), Cricktor (1), takuma sato (1)
 #2

-On the airgap laptop, run "listdescriptors false" on this updated wallet.dat file (this supposedly guarantees there are no private keys or anything that could compromise the funds)
That's right.

My question is: How do you triple check there are no private keys ever touching your online node or anything that could be used as an attack vector?
The first thing that you can do is to understand exactly what is happening here. You can do this by comparing the output of b]listdescriptors true[/b] and listdescriptors false on the airgapped machine. This is what you will have:
  • With listdescriptors true you will see results like this  "desc: pkh(xprv9..". Xprv indicates that it is a master private key.
  • With listdescriptors false you will see results like this "desc: pkh([0f1c015a/44h/0h/0h]xpub6B...". Xpub indicates that it is a master public key.

The second thing comes after you have imported the public descriptors to the watch-only wallet. Run getwalletinfo on this wallet, you will get something like this:
Quote
"private_keys_enabled": false,
"descriptors": true,

The third thing is also done on this watch-only wallet. Run listdescriptors true. You will get an error like this:
Quote
Can't get descriptor string. (code -4)

Should you never for instance use raw PSBT text and stick to the .psbt files?
You can use them, but using .psbt is better for various reasons. Some are more significant than others.

Any other things to check that this whole thing is running as intended?
No, I think I covered everything pretty well. Make sure not to confuse which wallet.dat is which and expose something by accident. Give them custom names.



For reference I did all these steps in order to write this post except the migrate wallet step.

flapduck
Member
**
Offline Offline

Activity: 78
Merit: 42


View Profile
October 23, 2025, 08:48:10 PM
Merited by takuma sato (1)
 #3

Looks like a good path.. Two extra sanity checks I use before letting a watch-only touch the network: try to do things that require keys and confirm hard failure. In Core, dumpwallet should error with "private keys are disabled," and signrawtransactionwithwallet on a dummy tx should refuse to sign.

And one more tip that might be helpful: PSBTs can leak derivation paths and global xpubs. If you ever hand a PSBT to anything other than your offline box, create it with bip32derivs=false or strip derivs first. That keeps your keys unspilled and your privacy intact.
nc50lc
Legendary
*
Offline Offline

Activity: 2968
Merit: 7992


Self-proclaimed Genius


View Profile
October 24, 2025, 03:23:41 AM
Merited by vapourminer (1), hugeblack (1), Cricktor (1), takuma sato (1)
 #4

Should you never for instance use raw PSBT text and stick to the .psbt files?
Those two are basically the same, it's just the PSBT in the former is presented in text while the latter is in binary format.
So it's up to your preference on which one is more convenient to export and import.

Any other things to check that this whole thing is running as intended?
If you imported those descriptors that have no private keys to a non-watch-only wallet (vice-versa), it will fail to import with a related warning.
That alone should be a good indicator that you've properly created a watch-only wallet and didn't exported the wrong descriptors.

And if you're worried that you somehow created a non-watch-only wallet and imported descriptors with private keys.
Check if there's an "eye" icon on the lower-right next to the "BTC" unit or lock symbol, that means that the selected wallet is watch-only.
like this:

takuma sato (OP)
Hero Member
*****
Offline Offline

Activity: 813
Merit: 702


View Profile
October 25, 2025, 04:27:08 PM
Merited by flapduck (2), vapourminer (1)
 #5

Looks like a good path.. Two extra sanity checks I use before letting a watch-only touch the network: try to do things that require keys and confirm hard failure. In Core, dumpwallet should error with "private keys are disabled," and signrawtransactionwithwallet on a dummy tx should refuse to sign.

And one more tip that might be helpful: PSBTs can leak derivation paths and global xpubs. If you ever hand a PSBT to anything other than your offline box, create it with bip32derivs=false or strip derivs first. That keeps your keys unspilled and your privacy intact.

Could you develop on this?

To do a PSBT transaction you are going to need to place the file on your online box last time I checked, so it's not like you can avoid this. I did a number of test transactions on the testnet to practice the PSBT workflow and I think it was something like this:

0) Enable the "Enable PSBT Controls" checkbox
1) Go on the watch-only wallet and with Coin Control pick addresses as needed.
2) Click on "Create unsigned" and save .psbt file
3) Get this file into the airgap laptop and sign with the "File->Load PSBT from file..." GUI menu by clicking on "Sign Tx"
4) Save this now signed psbt file and move it back into watch-only laptop wallet
5) Load the file with "File->Load PSBT from file..." and click on "Broadcast Tx"

So as you can see, the psbt file is created on the online wallet already since step 1, so I don't understand why you claim " If you ever hand a PSBT to anything other than your offline box" since it's unavoidable, and I also do not understand what the other "bip32derivs=false or strip derivs first." thing does. If you could please elaborate and explain what this does and exact steps.
takuma sato (OP)
Hero Member
*****
Offline Offline

Activity: 813
Merit: 702


View Profile
October 27, 2025, 07:11:03 PM
 #6

I did some diggin on the bip32derivs=false thing, so looks like psbt may potentially leak enough data that it could compromise you due derivation attacks, great. So could someone explain the exact extra steps to guarantee this does not happen? Relevant links:


https://github.com/bitcoin/bitcoin/issues/30294
https://bitcoin.stackexchange.com/questions/123383/why-are-bip32-derivs-included-in-psbt-outputs

Something about change addreses mentioned here:


Quote
The BIP32 derivation field in PSBT outputs is useful to be able to re-derive the address used in the corresponding transaction output. For instance it is useful for a signing device to verify the change address.

So what about that? I want to mitigate risks but not end up with change addresses that are do not match between the watch-only and airgap wallet. So could someone explain the workflow for this?
nc50lc
Legendary
*
Offline Offline

Activity: 2968
Merit: 7992


Self-proclaimed Genius


View Profile
October 28, 2025, 04:49:19 AM
 #7

I did some diggin on the bip32derivs=false thing, so looks like psbt may potentially leak enough data that it could compromise you due derivation attacks, great.
That is mostly a privacy issue.
The security issue is if you've exported and leaked at least one of the (non-loose) private key from that wallet.
But you wont just export those and copy to any online environment I presume.

Quote from: takuma sato
So what about that? I want to mitigate risks but not end up with change addresses that are do not match between the watch-only and airgap wallet. So could someone explain the workflow for this?
Bitcoin Knots keeps a huge keypool with 1000 gap limit per descriptor so it will know if any of the outputs doesn't match your change addresses even without "bip32derivs".
So then in your workflow above during the signing process, just check the displayed addresses if there's a note saying: "(own address)".
If you expect a change address and none of that showed, then do not sign the transaction.

Anyways, to use that parameter, you'll need to use walletcreatefundedpsbt command to create the PSBT since the GUI doesn't have such option and just defaults to 'true'.
e.g. (in the console):
Code:
walletcreatefundedpsbt "[]" "[{\"bcrt1q...28qqy8kt\":3.7331}]" 0 "{}" "false"

flapduck
Member
**
Offline Offline

Activity: 78
Merit: 42


View Profile
October 29, 2025, 11:34:37 AM
Merited by vapourminer (1)
 #8

Looks like a good path.. Two extra sanity checks I use before letting a watch-only touch the network: try to do things that require keys and confirm hard failure. In Core, dumpwallet should error with "private keys are disabled," and signrawtransactionwithwallet on a dummy tx should refuse to sign.

And one more tip that might be helpful: PSBTs can leak derivation paths and global xpubs. If you ever hand a PSBT to anything other than your offline box, create it with bip32derivs=false or strip derivs first. That keeps your keys unspilled and your privacy intact.

Could you develop on this?

To do a PSBT transaction you are going to need to place the file on your online box last time I checked, so it's not like you can avoid this. I did a number of test transactions on the testnet to practice the PSBT workflow and I think it was something like this:

0) Enable the "Enable PSBT Controls" checkbox
1) Go on the watch-only wallet and with Coin Control pick addresses as needed.
2) Click on "Create unsigned" and save .psbt file
3) Get this file into the airgap laptop and sign with the "File->Load PSBT from file..." GUI menu by clicking on "Sign Tx"
4) Save this now signed psbt file and move it back into watch-only laptop wallet
5) Load the file with "File->Load PSBT from file..." and click on "Broadcast Tx"

So as you can see, the psbt file is created on the online wallet already since step 1, so I don't understand why you claim " If you ever hand a PSBT to anything other than your offline box" since it's unavoidable, and I also do not understand what the other "bip32derivs=false or strip derivs first." thing does. If you could please elaborate and explain what this does and exact steps.



Confirmed, PSBTs can carry two kinds of wallet-structure data: per-key BIP32 derivation paths and a global xpubs map. Both aid signers, and both reveal account structure to anyone who sees the PSBT.
In Bitcoin Core, `walletcreatefundedpsbt` has a `bip32derivs` switch. It was default *false* in 0.19, and is default *true* by 22-23+. Set it to *false* on the watch-only box if you don't want derivation paths in the PSBT.
Derivation data on *outputs* lets a device verify the change address. If you omit it, you lose that automatic check, so verify change manually or use a fixed change address from your descriptor.


Flow that matches your setup:

1). Create PSBT on the watch-only wallet with derivs off.
Code:
bitcoin-cli -rpcwallet=watch -named walletcreatefundedpsbt \
  inputs='[]' outputs='{"bc1...":0.001}' options='{"add_inputs":true}' bip32derivs=false

2). Check it contains no xpubs or derivs:
Code:
bitcoin-cli decodepsbt "$PSBT" | grep -Ei 'xpub|bip32_derivs'  # no hits

3). Move PSBT to the air-gapped wallet and sign:  
Code:
SIGNED=$(bitcoin-cli -rpcwallet=airgap walletprocesspsbt "$PSBT" true | jq -r .psbt)
RAW=$(bitcoin-cli finalizepsbt "$SIGNED" true | jq -r .hex)

4). Broadcast only the raw hex from the online node:
Code:
bitcoin-cli sendrawtransaction "$RAW"
takuma sato (OP)
Hero Member
*****
Offline Offline

Activity: 813
Merit: 702


View Profile
November 14, 2025, 05:08:58 AM
Merited by vapourminer (4)
 #9

Looks like a good path.. Two extra sanity checks I use before letting a watch-only touch the network: try to do things that require keys and confirm hard failure. In Core, dumpwallet should error with "private keys are disabled," and signrawtransactionwithwallet on a dummy tx should refuse to sign.

And one more tip that might be helpful: PSBTs can leak derivation paths and global xpubs. If you ever hand a PSBT to anything other than your offline box, create it with bip32derivs=false or strip derivs first. That keeps your keys unspilled and your privacy intact.

Could you develop on this?

To do a PSBT transaction you are going to need to place the file on your online box last time I checked, so it's not like you can avoid this. I did a number of test transactions on the testnet to practice the PSBT workflow and I think it was something like this:

0) Enable the "Enable PSBT Controls" checkbox
1) Go on the watch-only wallet and with Coin Control pick addresses as needed.
2) Click on "Create unsigned" and save .psbt file
3) Get this file into the airgap laptop and sign with the "File->Load PSBT from file..." GUI menu by clicking on "Sign Tx"
4) Save this now signed psbt file and move it back into watch-only laptop wallet
5) Load the file with "File->Load PSBT from file..." and click on "Broadcast Tx"

So as you can see, the psbt file is created on the online wallet already since step 1, so I don't understand why you claim " If you ever hand a PSBT to anything other than your offline box" since it's unavoidable, and I also do not understand what the other "bip32derivs=false or strip derivs first." thing does. If you could please elaborate and explain what this does and exact steps.



Confirmed, PSBTs can carry two kinds of wallet-structure data: per-key BIP32 derivation paths and a global xpubs map. Both aid signers, and both reveal account structure to anyone who sees the PSBT.
In Bitcoin Core, `walletcreatefundedpsbt` has a `bip32derivs` switch. It was default *false* in 0.19, and is default *true* by 22-23+. Set it to *false* on the watch-only box if you don't want derivation paths in the PSBT.
Derivation data on *outputs* lets a device verify the change address. If you omit it, you lose that automatic check, so verify change manually or use a fixed change address from your descriptor.


Flow that matches your setup:

1). Create PSBT on the watch-only wallet with derivs off.
Code:
bitcoin-cli -rpcwallet=watch -named walletcreatefundedpsbt \
  inputs='[]' outputs='{"bc1...":0.001}' options='{"add_inputs":true}' bip32derivs=false

2). Check it contains no xpubs or derivs:
Code:
bitcoin-cli decodepsbt "$PSBT" | grep -Ei 'xpub|bip32_derivs'  # no hits

3). Move PSBT to the air-gapped wallet and sign:  
Code:
SIGNED=$(bitcoin-cli -rpcwallet=airgap walletprocesspsbt "$PSBT" true | jq -r .psbt)
RAW=$(bitcoin-cli finalizepsbt "$SIGNED" true | jq -r .hex)

4). Broadcast only the raw hex from the online node:
Code:
bitcoin-cli sendrawtransaction "$RAW"


I would need to double check the two posts above to understand what's going on but the way I see it is that you need the change address to match your airgap wallet with the online node wallet. If you do not have matching change addresses, you would lose the funds since they may end up in an address that you do not have private keys for.

Using a fixed change address that you create before making the transaction to leak less data on the psbt would be a way to go. But you could screw up in this process trying to leak less data on the psbt file. If you do not use a fixed change address, then in theory, the address used for change should exist on both online and offline wallets even if you do not have this "(own address)" check right? this is only to have this sanity check and has nothing to do with anything else? because in theory, when you create a watch-only wallet by importing the output of "listdescriptors false" from your airgap wallet with "importdescriptors", then when you generate addresses on the online wallet, they will forever be matching with the keys you generate on your airgap wallet right?

So this thing on the psbt is just to get this "(own address)" sanity check only just for optics? Because in that case, you could disable this, and just look on your airgap wallet laptop if you own the private key for that public change address manually, or plan ahead and generate a change address, it's one of these 2 solutions. You would just need to more attention, but perhaps worth it if you avoid leaking stuff on your psbt file which will be on your online node at least until you broadcast the transaction when you can shred it.

Also this "bip32derivs=false" you run in the console command or where does this go?
I create the PSBT files using Coin Control so hopefully I don't need to create these manually to do this.
Cricktor
Legendary
*
Offline Offline

Activity: 1316
Merit: 3168



View Profile
November 15, 2025, 08:49:27 PM
Last edit: November 16, 2025, 07:15:10 PM by Cricktor
 #10

Also this "bip32derivs=false" you run in the console command or where does this go?
These are named parameters for the RPC commands that are invoked with bitcoin-cli ... -named ... option. It allows you to specify optional or non-optional parameters to a Bitcoin Core command regardless of their position in the invoking command and especially if you want to omit certain optional parameters and want to avoid positional missinterpretation.

See help walletcreatefundedpsbt in Core's console or bitcoin-cli help walletcreatefundedpsbt on shell command-line for arguments and "named arguments" in particular.

nc50lc
Legendary
*
Offline Offline

Activity: 2968
Merit: 7992


Self-proclaimed Genius


View Profile
November 16, 2025, 04:35:41 AM
 #11

Also this "bip32derivs=false" you run in the console command or where does this go?
I create the PSBT files using Coin Control so hopefully I don't need to create these manually to do this.
The example he provided contains "bitcoin-cli" (you can't execute bitcoin-cli through bitcoin-qt's console)
And it contains the --named arg, so it goes in your terminal.

For the GUI's console, refer to my example command just above his reply.
The 5th args "false" is the value for "bip32derivs", type it with or without the quotes (string/bool), Knots will be able to parse it either way.

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!