Skip to content

Recommended PSBT Signing Flow

Overview

  • Lead Developer: @oleonardolima
  • Ticket: #70
  • Pull Request: #235
  • Feature Type: Non-Breaking (workflow recommendation)

Overview

BDK 3.0 recommends a new way of signing transactions, keeping private key material out of the Wallet entirely. Use public descriptors in the Wallet for address derivation and coin tracking, build transactions with TxBuilder to obtain a PSBT, and then sign that PSBT using the Psbt::sign API from rust-bitcoin.

Why Do This?

In previous workflows it was common to construct a Wallet with a private descriptor, letting BDK both track coins and sign transactions. Mixing these responsibilities has a few downsides:

  • Private key material lives in the same process as network I/O and coin selection. This is not necessary, and keeping the responsibility for private keys separate is simply good practice.
  • It doesn't model most real signing architectures. In practice, keys often live on a hardware device, in a separate process, or behind an HSM. Adding them to the Wallet for the duration of the process is unecessary, and many applications were already separating these concerns in less ergonomic ways than is now possible.
  • PSBTs exist precisely to separate these roles. The PSBT format was designed so that an unsigned transaction can be handed off to one or more independent signers, and keeping that separation in code makes the design explicit.

1. Create the Wallet with Public Descriptors

Use public descriptors to create your Wallet:

use bdk_wallet::Wallet;
use bitcoin::Network;

let descriptor = Miniscript12Descriptor::<Miniscript12DescriptorPublicKey>::from_str("tr([5bc5d243/86'/1'/0']tpubDC72NVP1.../0/*)#xh44xwsp").unwrap();

let wallet = Wallet::create_single(descriptor)
    .network(Network::Regtest)
    .create_wallet_no_persist()?;

The Wallet can now derive addresses, track UTXOs, and build transactions without ever holding a private key.

2. Build a PSBT with TxBuilder

use bdk_wallet::wallet::tx_builder::TxOrdering;
use bitcoin::Amount;

let recipient = Address::from_str("bc1q...")?.assume_checked();

let mut psbt = {
    let mut builder = wallet.build_tx();
    builder
        .add_recipient(recipient.script_pubkey(), Amount::from_sat(50_000));
    builder.finish()?
};

3. Sign with Psbt::sign

Load your private key separately — from secure storage, a hardware wallet integration, or an air-gapped device, and call Psbt::sign directly on the PSBT:

// You must use miniscript 13.0.0 or above for this to work, see https://github.com/rust-bitcoin/rust-miniscript/pull/851
let secp = bitcoin::secp256k1::Secp256k1::new();
let result: (Descriptor<DescriptorPublicKey>, KeyMap) = Descriptor::parse_descriptor(&secp, "tr(tprv8ZgxMBicQKsPdWAHbugK.../86'/1'/0'/0/*)#x627tk5a").unwrap();
let keymap = &result.1;
psbt.sign(keymap, &secp);

Psbt::sign accepts anything that implements rust-bitcoin's GetKey trait.

4. Finalize and Broadcast

use bdk_wallet::miniscript::psbt::PsbtExt;

let finalized = psbt.finalize(&secp)?;
let tx = psbt.extract_tx()?;
client.broadcast(&tx)?;

Summary

Step Responsibility API
Address derivation & coin tracking Wallet (public descriptor) Wallet::create
Transaction building Wallet wallet.build_tx()
Signing Your key store Psbt::sign (rust-bitcoin)
Finalization & broadcast Wallet + client wallet.finalize_psbt, client.broadcast

This separation means your signing code can be swapped out or upgraded independently of your wallet logic, and private key material never needs to touch the same code paths as network I/O or persistence.