Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ use bitcoin::{
psbt,
secp256k1::Secp256k1,
sighash::{EcdsaSighashType, TapSighashType},
transaction, Address, Amount, Block, FeeRate, Network, NetworkKind, OutPoint, Psbt, ScriptBuf,
Sequence, SignedAmount, Transaction, TxOut, Txid, Weight, Witness,
transaction::{self, Version},
Address, Amount, Block, FeeRate, Network, NetworkKind, OutPoint, Psbt, ScriptBuf, Sequence,
SignedAmount, Transaction, TxOut, Txid, Weight, Witness,
};
use miniscript::{
descriptor::KeyMap,
Expand Down Expand Up @@ -1429,7 +1430,7 @@ impl Wallet {
let (required_utxos, optional_utxos) = {
// NOTE: manual selection overrides unspendable
let mut required: Vec<WeightedUtxo> = params.utxos.clone();
let optional = self.filter_utxos(&params, current_height.to_consensus_u32());
let optional = self.filter_utxos(&params, current_height.to_consensus_u32(), version);

// If `drain_wallet` is true, all UTxOs are required.
if params.drain_wallet {
Expand Down Expand Up @@ -2029,7 +2030,12 @@ impl Wallet {

/// Given the options returns the list of utxos that must be used to form the
/// transaction and any further that may be used if needed.
fn filter_utxos(&self, params: &TxParams, current_height: u32) -> Vec<WeightedUtxo> {
fn filter_utxos(
&self,
params: &TxParams,
current_height: u32,
version: Version,
) -> Vec<WeightedUtxo> {
if params.manually_selected_only {
vec![]
// Only process optional UTxOs if manually_selected_only is false.
Expand All @@ -2039,6 +2045,7 @@ impl Wallet {
.iter()
.map(|wutxo| wutxo.utxo.outpoint())
.collect::<HashSet<OutPoint>>();

self.tx_graph
.graph()
// Get all unspent UTxOs from wallet.
Expand All @@ -2058,6 +2065,27 @@ impl Wallet {
.is_mature(current_height)
.then(|| new_local_utxo(k, i, full_txo))
})
// only add to optional UTXOs those that follows BIP-431 (TRUC) specification.
// see https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki#specification
.filter(|local_output| {
if local_output.chain_position.is_confirmed() {
return true;
}

let Some(ancestor_tx) = self.tx_graph().get_tx(local_output.outpoint.txid)
// if we don't have the full tx available we can't assure the ancestor
// tx version it assumes it's a valid candidate.
else {
return true;
};

match is_truc(version) {
// if building TRUC; filter out all unconfirmed non-TRUC.
true => is_truc(ancestor_tx.version),
// if building non-TRUC; filter out all unconfirmed TRUC.
false => !is_truc(ancestor_tx.version),
}
})
// only process UTXOs not selected manually, they will be considered later in the
// chain
// NOTE: this avoid UTXOs in both required and optional list
Expand Down Expand Up @@ -2942,6 +2970,12 @@ fn make_indexed_graph(
Ok(indexed_graph)
}

/// Check if the given [`transaction::Version`] is TRUC (Topologically Restricted Until
/// Confirmation).
fn is_truc(version: transaction::Version) -> bool {
version.eq(&Version(3))
}

/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
#[macro_export]
#[doc(hidden)]
Expand Down Expand Up @@ -3038,8 +3072,13 @@ mod test {
let mut builder = wallet.build_tx();
builder.add_utxo(outpoint).expect("should add local utxo");
let params = builder.params.clone();
let version = params.version.unwrap_or(Version::TWO);
// enforce selection of first output in transaction
let received = wallet.filter_utxos(&params, wallet.latest_checkpoint().block_id().height);
let received = wallet.filter_utxos(
&params,
wallet.latest_checkpoint().block_id().height,
version,
);
// Notice expected doesn't include the first output from two_output_tx as it should be
// filtered out.
let expected = vec![wallet
Expand Down
139 changes: 139 additions & 0 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::sync::Arc;
use assert_matches::assert_matches;
use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime};
use bdk_wallet::coin_selection;
use bdk_wallet::coin_selection::InsufficientFunds;
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
use bdk_wallet::error::CreateTxError;
use bdk_wallet::psbt::PsbtUtils;
Expand Down Expand Up @@ -3059,3 +3060,141 @@ fn test_tx_ordering_untouched_preserves_insertion_ordering_bnb_success() {
"UTXOs should be ordered with required first, then selected"
);
}

#[test]
fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> {
let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc();
let mut wallet = Wallet::create(descriptor, change_descriptor)
.network(Network::Regtest)
.create_wallet_no_persist()
.expect("should create wallet successfully!");

// establish a chain tip so confirmed funds can be anchored to a block in the active chain.
let block = BlockId {
height: 1_000,
hash: BlockHash::all_zeros(),
};
insert_checkpoint(&mut wallet, block);
let anchor = ConfirmationBlockTime {
block_id: block,
confirmation_time: 0,
};

// add funds to the wallet (two 250k sats confirmed UTXOs)
receive_output(&mut wallet, Amount::from_sat(250_000), anchor);
receive_output(&mut wallet, Amount::from_sat(250_000), anchor);

let balance = wallet.balance();
assert_eq!(
balance.total(),
Amount::from_sat(500_000),
"wallet balance SHOULD be 500K after funding"
);

// Should be able to create TRUC (v3) transactions.
// NOTE: "A TRUC transaction can spend outputs from confirmed non-TRUC transactions. A non-TRUC
// transaction can spend outputs from confirmed TRUC transactions" See, rule #2: https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki#specification

// create txA (TRUC)
let recv_addr = wallet.next_unused_address(KeychainKind::External);

let mut builder = wallet.build_tx();
builder.add_recipient(recv_addr.script_pubkey(), Amount::from_sat(125_000));
builder.version(3);

let mut psbt = builder.finish().expect("should create txA (TRUC) successfully! as per BIP-431 it can spend confirmed outputs from non-TRUC txs.");

let _ = wallet.sign(&mut psbt, SignOptions::default())?;
let tx_a = psbt.extract_tx()?;
let txid_a = tx_a.compute_txid();

// "broadcast" txA (TRUC): insert it into the wallet's local view as an unconfirmed tx.
insert_tx(&mut wallet, tx_a);

let balance = wallet.balance();
assert_eq!(
balance.untrusted_pending,
Amount::from_sat(125_000),
"wallet balance SHOULD have 125K unconfirmed (TRUC) UTXO after txA!"
);

// create txB (non-TRUC)
let recv_addr = wallet.next_unused_address(KeychainKind::External);

let mut builder = wallet.build_tx();
builder.add_recipient(recv_addr.script_pubkey(), Amount::from_sat(125_000));

let mut psbt = builder
.finish()
.expect("SHOULD create txB (non-TRUC) successfully! However, a non-TRUC transaction can only spend confirmed outputs from TRUC transactions");

let _ = wallet.sign(&mut psbt, SignOptions::default());
let tx_b = psbt.extract_tx()?;

// txB MUST NOT use the available unconfirmed TRUC UTXO.
assert!(
tx_b.input
.iter()
.all(|txin| txin.previous_output.txid.ne(&txid_a)),
"SHOULD NOT try to spend an unconfirmed TRUC output in a non-TRUC tx!"
);

// "broadcast" txB (non-TRUC)
let txid_b = tx_b.compute_txid();
insert_tx(&mut wallet, tx_b);

let balance = wallet.balance();
assert_eq!(
balance.untrusted_pending,
Amount::from_sat(250_000),
"wallet balance SHOULD have 250K unconfirmed, both non-TRUC (txB) and TRUC (txA) UTXOs after txB!"
);

// create txC (TRUC)
let recv_addr = wallet.next_unused_address(KeychainKind::External);

let mut builder = wallet.build_tx();
builder.add_recipient(recv_addr.script_pubkey(), Amount::from_sat(200_000));
builder.version(3);

let mut psbt = builder.finish().expect("should create txC (TRUC) successfully! as per BIP-431 it can spend unconfirmed outputs from TRUC txs.");

let _ = wallet.sign(&mut psbt, SignOptions::default())?;
let tx_c = psbt.extract_tx()?;

// txC MUST ONLY use the available confirmed UTXOs AND/OR unconfirmed TRUC UTXOs.
assert!(
tx_c.input
.iter()
.all(|txin| txin.previous_output.txid.ne(&txid_b)),
"SHOULD NOT try to spend an unconfirmed non-TRUC output in a TRUC tx!"
);

// "broadcast" txC (TRUC)
insert_tx(&mut wallet, tx_c);

let balance = wallet.balance();
assert_eq!(
balance.untrusted_pending,
Amount::from_sat(325_000),
"wallet balance SHOULD have 325K unconfirmed UTXOs after txC!"
);

// create txD (non-TRUC)
let recv_addr = wallet.next_unused_address(KeychainKind::External);

let mut builder = wallet.build_tx();
builder.add_recipient(recv_addr.script_pubkey(), Amount::from_sat(400_000));

let psbt = builder.finish();

assert!(
matches!(
psbt,
Err(CreateTxError::CoinSelection(InsufficientFunds { .. }))
),
"SHOULD fail if it's trying to spend an unconfirmed TRUC output in a non-TRUC tx!"
);

Ok(())
}