Files
bitcoin/doc/offline-signing-tutorial.md
Pablo Martin 68cb7840d2 doc: improve offline-signing-tutorial after 32489
General improvements noted in the #32489 review and deferred by the
author:

- Remove a stale NOTE referencing walletcreatefundedpsbt; the tutorial
  was updated to use the send RPC instead.
- Fix listtransactions example output from {...} to [...]; the RPC
  returns a JSON array, not an object.
2026-07-03 12:30:17 -03:00

9.3 KiB

Offline Signing Tutorial

This tutorial will describe how to use two instances of Bitcoin Core, one online and one offline, to greatly increase security by not having private keys reside on a networked device.

Maintaining an air-gap between private keys and any network connections drastically reduces the opportunity for those keys to be exfiltrated from the user.

This workflow uses Partially Signed Bitcoin Transactions (PSBTs) to transfer the transaction to and from the offline wallet for signing using the private keys.

Note

While this tutorial demonstrates the process using signet network, you should omit the -signet flag in the provided commands when working with mainnet.

Overview

In this tutorial we have two hosts, both running Bitcoin v32.0

  • offline host which is disconnected from all networks (internet, Tor, wifi, bluetooth etc.) and does not have, or need, a copy of the blockchain.
  • online host which is a regular online node with a synced blockchain.

We are going to first create an offline_wallet on the offline host. We will then create a watch_only_wallet on the online host using a wallet file exported from the offline_wallet. Next we will receive some coins into the wallet. In order to spend these coins we'll create an unsigned PSBT using the watch_only_wallet, sign the PSBT using the private keys in the offline_wallet, and finally broadcast the signed PSBT using the online host.

Requirements

  • jq installation - This tutorial uses jq to process certain fields from JSON RPC responses, but this convenience is optional.

Create and Prepare the offline_wallet

  1. On the offline machine create a wallet named offline_wallet secured by a wallet passphrase. This wallet will contain private keys and must remain unconnected to any networks at all times.
[offline]$ ./build/bin/bitcoin-cli -signet -named createwallet \
                wallet_name="offline_wallet" \
                passphrase="** enter passphrase **"

{
  "name": "offline_wallet"
}

bitcoin rpc can also be substituted for bitcoin-cli.

Note

The use of a passphrase is crucial to encrypt the wallet.dat file. This encryption ensures that even if an unauthorized individual gains access to the offline host, they won't be able to access the wallet's contents. Further details about securing your wallet can be found in Managing the Wallet

  1. Export the wallet in a watch-only format to a .dat file named watch_only_wallet.dat.
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" -named exportwatchonlywallet \
             destination=/path/to/watch_only_wallet.dat

Note

The watch_only_wallet.dat file will be transferred to the online machine (e.g. using a USB flash drive) where it can be imported to create a related watch-only wallet.

Create the online watch_only_wallet

On the online machine import the watch-only wallet. This wallet will have the private keys disabled and is named watch_only_wallet. This is achieved by using the restorewallet rpc call. The watch_only_wallet wallet will be used to track and validate incoming transactions, create unsigned PSBTs when spending coins, and broadcast signed and finalized PSBTs.

[online]$ ./build/bin/bitcoin-cli -signet -named restorewallet \
              wallet_name="watch_only_wallet" \
              backup_file=/path/to/watch_only_wallet.dat

{
  "name": "watch_only_wallet"
}

Fund the offline_wallet

At this point, it's important to understand that both the offline_wallet and online watch_only_wallet share the same public keys. As a result, they generate the same addresses. Transactions can be created using either wallet, but valid signatures can only be added by the offline_wallet as only it has the private keys.

  1. Generate an address to receive coins. You can use either the offline_wallet or the online watch_only_wallet to generate this address, as they will produce the same addresses. For the sake of this guide, we'll use the online watch_only_wallet to generate the address.
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" getnewaddress

tb1qtu5qgc6ddhmqm5yqjvhg83qgk2t4ewajg0h6yh
  1. Visit a faucet like https://signetfaucet.com and enter your address from the previous command to receive a small amount of signet coins to this address.

  2. Confirm that coins were received using the online watch_only_wallet. Note that the transaction may take a few moments before being received on your local node, depending on its connectivity. Just re-run the command periodically until the transaction is received.

[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" listunspent

[
  {
    "txid": "0f3953dfc3eb8e753cd1633151837c5b9953992914ff32b7de08c47f1f29c762",
    "vout": 1,
    "address": "tb1qtu5qgc6ddhmqm5yqjvhg83qgk2t4ewajg0h6yh",
    "label": "",
    "scriptPubKey": "00145f2804634d6df60dd080932e83c408b2975cbbb2",
    "amount": 0.01000000,
    "confirmations": 4,
    "spendable": true,
    "solvable": true,
    "desc": "wpkh([306c734f/84h/1h/0h/0/0]025932ccee7590158f7e08bb36290d135d30a0b045163da896e1cd7645ec4223a9)#xytvyr4a",
    "parent_descs": [
      "wpkh([306c734f/84h/1h/0h]tpubDCJnY92ib4Zu3qd6wrBXEjG436tQdA2tDiJU2iSJYjkNS1darssPWKaBfojhjUF5vMLBcxbN2r93pmFMz2zyTEZuNx9JDo9rWqoHhATW3Uz/0/*)#7mh08dkg"
    ],
    "safe": true
  }
]

Create and Export an Unsigned PSBT

  1. Get a destination address for the transaction. In this tutorial we'll be sending funds to the address tb1q9k5w0nhnhyeh78snpxh0t5t7c3lxdeg3erez32, but if you don't need the coins for further testing you could send the coins back to the faucet.

  2. Create a funded but unsigned PSBT to the destination address with the online watch_only_wallet by using send [{"address":amount},...] and export the unsigned PSBT to a file funded_psbt.txt for easy portability to the offline_wallet for signing:

[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" send \
              '{"tb1q9k5w0nhnhyeh78snpxh0t5t7c3lxdeg3erez32": 0.009}' \
              | jq -r '.psbt' \
              >> /path/to/funded_psbt.txt

[online]$ cat /path/to/funded_psbt.txt

cHNidP8BAHECAAAAAWLHKR9/xAjetzL/FCmZU5lbfINRMWPRPHWO68PfUzkPAQAAAAD9////AoA4AQAAAAAAFgAULajnzvO5M38eEwmu9dF+xH5m5RGs0g0AAAAAABYAFMaT0f/Wp2DCZzL6dkJ3GhWj4Y9vAAAAAAABAHECAAAAAY+dRPEBrGopkw4ugSzS9npzJDEIrE/bq1XXI0KbYnYrAQAAAAD+////ArKaXgAAAAAAFgAUwEc4LdoxSjbWo/2Ue+HS+QjwfiBAQg8AAAAAABYAFF8oBGNNbfYN0ICTLoPECLKXXLuyYW8CAAEBH0BCDwAAAAAAFgAUXygEY01t9g3QgJMug8QIspdcu7IiBgJZMszudZAVj34IuzYpDRNdMKCwRRY9qJbhzXZF7EIjqRgwbHNPVAAAgAEAAIAAAACAAAAAAAAAAAAAACICA7BlBnyAR4F2UkKuSX9MFhYCsn6j//z9i7lHDm1O0CU0GDBsc09UAACAAQAAgAAAAIABAAAAAAAAAAA=

Decode and Analyze the Unsigned PSBT

Decode and analyze the unsigned PSBT on the offline_wallet using the funded_psbt.txt file:

[offline]$ ./build/bin/bitcoin-cli -signet decodepsbt $(cat /path/to/funded_psbt.txt)

{
    ...
}

[offline]$ ./build/bin/bitcoin-cli -signet analyzepsbt $(cat /path/to/funded_psbt.txt)

{
  "inputs": [
    {
      "has_utxo": true,
      "is_final": false,
      "next": "signer",
      "missing": {
        "signatures": [
          "5f2804634d6df60dd080932e83c408b2975cbbb2"
        ]
      }
    }
  ],
  "estimated_vsize": 141,
  "estimated_feerate": 0.00100000,
  "fee": 0.00014100,
  "next": "signer"
}

Notice that the analysis of the PSBT shows that "signatures" are missing and should be provided by the private key corresponding to the public key hash (hash160) "5f2804634d6df60dd080932e83c408b2975cbbb2"

Process and Sign the PSBT

  1. Unlock the offline_wallet with the Passphrase:

Use the walletpassphrase command to unlock the offline_wallet with the passphrase. You should specify the passphrase and a timeout (in seconds) for how long you want the wallet to remain unlocked.

[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" walletpassphrase "** enter passphrase **" 60
  1. Process, sign and finalize the PSBT on the offline_wallet using the walletprocesspsbt command, saving the output to a file final_psbt.txt.
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" walletprocesspsbt \
               $(cat /path/to/funded_psbt.txt) \
               | jq -r .hex \
               >> /path/to/final_psbt.txt

Broadcast the Signed and Finalized PSBT

Broadcast the funded, signed and finalized PSBT final_psbt.txt using sendrawtransaction with an online node:

[online]$ ./build/bin/bitcoin-cli -signet sendrawtransaction $(cat /path/to/final_psbt.txt)

c2430a0e46df472b04b0ca887bbcd5c4abf7b2ce2eb71de981444a80e2b96d52

Confirm Wallet Balance

Confirm the updated balance of the offline wallet using the watch_only_wallet.

[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" getbalances

{
  "mine": {
    "trusted": 0.00085900,
    "untrusted_pending": 0.00000000,
    "immature": 0.00000000
  },
  "lastprocessedblock": {
    "hash": "0000003065c0669fff27edb4a71928cb48e5a6cfcdf06f491a83fd86822d18a6",
    "height": 159592
  }
}

You can also show transactions related to the wallet using listtransactions

[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" listtransactions

[
    ...
]