Skip to main content
Offline signing (also called “cold storage signing” or “air-gapped signing”) dramatically increases security by keeping private keys on a device that never connects to any network. This guide demonstrates using two Bitcoin Core instances: one online (watch-only) and one completely offline (with private keys).

Overview

The offline signing workflow uses Partially Signed Bitcoin Transactions (PSBTs) to transfer transaction data between the online and offline environments:
Online Wallet (watch-only)    Offline Wallet (private keys)
         │                              │
         ├──── Create unsigned PSBT ────►│
         │                              │
         │                          Sign PSBT
         │                              │
         │◄──── Return signed PSBT ─────┤
         │                              │
    Broadcast                           │
Security Requirements:
  • Offline device must NEVER connect to internet, WiFi, Bluetooth, or any network
  • Transfer data only via USB drive, QR codes, or other physical media
  • Verify all transaction details on the offline device before signing

Prerequisites

  • Two devices with Bitcoin Core installed
  • USB drive or other air-gapped data transfer method
  • jq for JSON processing (optional)
This tutorial uses signet for demonstration. For mainnet, omit the -signet flag from all commands.

Setup: Create the Offline Wallet

1

Create encrypted wallet on offline device

On your offline machine, create a wallet with a strong passphrase:
[offline]$ bitcoin-cli -signet -named createwallet \
  wallet_name="offline_wallet" \
  passphrase="your-strong-passphrase"
The passphrase encrypts your wallet.dat file. Without it, even an attacker with physical access cannot spend your bitcoin. Choose a strong, unique passphrase and store it securely.
2

Export public descriptors

Export the wallet’s public key descriptors to a file:
[offline]$ bitcoin-cli -signet -rpcwallet="offline_wallet" \
  listdescriptors | jq -r '.descriptors' \
  > descriptors.json
This creates a JSON file containing public key information only (no private keys).
3

Transfer descriptors to online device

Copy descriptors.json to a USB drive and physically transfer it to your online device.

Setup: Create the Watch-Only Wallet

1

Create blank watch-only wallet

On your online machine, create a wallet that can’t hold private keys:
[online]$ bitcoin-cli -signet -named createwallet \
  wallet_name="watch_only_wallet" \
  disable_private_keys=true \
  blank=true
2

Import descriptors from offline wallet

Import the public descriptors from the offline wallet:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" \
  importdescriptors "$(cat descriptors.json)"
You should see multiple "success": true responses.
3

Verify wallet setup

Both wallets now generate identical addresses:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" getnewaddress
The watch-only wallet can track transactions but cannot sign them.

Receiving Bitcoin

Generate a receiving address using either wallet (they produce the same addresses):
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" getnewaddress

tb1qtu5qgc6ddhmqm5yqjvhg83qgk2t4ewajg0h6yh
For signet testing, get coins from the faucet:
./contrib/signet/getcoins.py -a tb1qtu5qgc6ddhmqm5yqjvhg83qgk2t4ewajg0h6yh
Or visit https://signetfaucet.com Verify receipt:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" listunspent

Spending Bitcoin (Complete Workflow)

1

Create unsigned PSBT on online wallet

Create a funded but unsigned PSBT:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" send \
  '{"tb1q9k5w0nhnhyeh78snpxh0t5t7c3lxdeg3erez32": 0.009}' \
  | jq -r '.psbt' > funded_psbt.txt
This creates a transaction sending 0.009 BTC to the specified address.
2

Transfer PSBT to offline device

Copy funded_psbt.txt to your USB drive and physically move it to the offline machine.
3

Analyze PSBT on offline device

Before signing, verify the transaction details:
[offline]$ bitcoin-cli -signet decodepsbt $(cat funded_psbt.txt)
Check:
  • Output addresses are correct
  • Amounts match your intention
  • Fee is reasonable
Analyze what needs signing:
[offline]$ bitcoin-cli -signet analyzepsbt $(cat funded_psbt.txt)
4

Unlock wallet with passphrase

Unlock the offline wallet temporarily (60 seconds):
[offline]$ bitcoin-cli -signet -rpcwallet="offline_wallet" \
  walletpassphrase "your-strong-passphrase" 60
5

Sign the PSBT

Sign the transaction with your private keys:
[offline]$ bitcoin-cli -signet -rpcwallet="offline_wallet" \
  walletprocesspsbt $(cat funded_psbt.txt) \
  | jq -r .hex > signed_psbt.txt
The wallet automatically locks again after 60 seconds.
6

Transfer signed PSBT back to online device

Copy signed_psbt.txt to USB drive and move to online machine.
7

Broadcast the transaction

On the online machine, broadcast the signed transaction:
[online]$ bitcoin-cli -signet sendrawtransaction $(cat signed_psbt.txt)
This returns the transaction ID (txid).
8

Verify broadcast

Confirm the transaction was broadcast:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" \
  listtransactions

Checking Balance

View balance using the watch-only wallet:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" getbalances
This shows:
  • trusted - Confirmed balance (spendable)
  • untrusted_pending - Unconfirmed incoming
  • immature - Coinbase transactions still maturing

Alternative: Using walletcreatefundedpsbt

For more control, use walletcreatefundedpsbt instead of send:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" \
  walletcreatefundedpsbt \
  '[]' \
  '{"destination_address": 0.009}' \
  | jq -r '.psbt' > funded_psbt.txt
Leaving inputs empty [] allows automatic coin selection.

Security Best Practices

Critical Security Measures:Offline Device:
  • Never connect to any network (internet, WiFi, Bluetooth, etc.)
  • Disable all network hardware if possible
  • Keep in a physically secure location
  • Use full disk encryption
  • Store wallet backups securely and separately
Online Device:
  • Keep Bitcoin Core updated
  • Use firewall and antivirus
  • Regularly verify wallet is watch-only: getwalletinfo should show "private_keys_enabled": false
PSBT Transfer:
  • Use dedicated USB drive (no other files)
  • Scan USB for malware on online device before use
  • Consider using QR codes for small transactions
Transaction Verification:
  • Always decode and review PSBTs before signing
  • Verify recipient addresses independently
  • Check amounts and fees carefully
  • Don’t rush - take time to verify everything

Backup and Recovery

What to Backup

  1. Offline wallet seed phrase (if using HD wallet)
  2. Wallet passphrase (store separately from seed!)
  3. wallet.dat file from offline wallet (encrypted)
  4. Descriptors (for recreating watch-only wallet)

Recovery Process

If you need to recover:
  1. Restore offline wallet:
    # If you have wallet.dat
    [offline]$ cp backup/wallet.dat ~/.bitcoin/signet/wallets/offline_wallet/
    
    # Or restore from seed phrase (method varies by wallet creation)
    
  2. Export descriptors from restored wallet
  3. Recreate watch-only wallet on new online device
  4. Import descriptors as shown in setup section

Advanced: Multisig with Offline Signing

Combine offline signing with multisig for maximum security:
# Create 2-of-3 multisig where one key is always offline
# - Key 1: Online hot wallet
# - Key 2: Offline cold wallet  
# - Key 3: Backup key (offline, separate location)
See the Multisig guide for setup details.

Troubleshooting

”Insufficient funds” on Watch-Only Wallet

Ensure transactions have confirmations:
[online]$ bitcoin-cli -signet -rpcwallet="watch_only_wallet" \
  listunspent 1  # Minimum 1 confirmation

“Wallet is locked” When Signing

Unlock wallet with passphrase:
[offline]$ bitcoin-cli -signet -rpcwallet="offline_wallet" \
  walletpassphrase "passphrase" 120

“Private keys are disabled”

You’re trying to sign with the watch-only wallet. Always sign on the offline device.

PSBT Decode Error

Ensure PSBT string is complete and not corrupted during transfer. Check file size and compare checksums.

Alternative Offline Methods

QR Code Transfer

For small transactions, use QR codes:
  1. Generate PSBT on online device
  2. Display as QR code
  3. Scan with offline device camera
  4. Sign and encode as QR
  5. Scan QR back to online device
Note: Bitcoin Core doesn’t have built-in QR support; requires external tools.

Hardware Wallets

Hardware wallets provide similar security with better UX:
  • Purpose-built secure element
  • Screen for verification
  • Direct USB/Bluetooth signing
  • Support for multiple cryptocurrencies
Bitcoin Core supports hardware wallets via HWI.

Next Steps

PSBT Deep Dive

Learn more about PSBT internals and advanced usage

Multisig

Combine offline signing with multi-signature security