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
29 changes: 24 additions & 5 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,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 @@ -1978,18 +1978,24 @@ 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,
tx_version: transaction::Version,
) -> Vec<WeightedUtxo> {
if params.manually_selected_only {
vec![]
// Only process optional UTxOs if manually_selected_only is false.
} else {
let tx_graph = self.tx_graph.graph();
let excludes_unconfirmed_v3 = tx_version != transaction::Version(3);
let manually_selected_outpoints = params
.utxos
.iter()
.map(|wutxo| wutxo.utxo.outpoint())
.collect::<HashSet<OutPoint>>();
self.tx_graph
.graph()
tx_graph
// Get all unspent UTxOs from wallet.
// NOTE: the UTxOs returned by the following method already belong to wallet as the
// call chain uses get_tx_node infallibly.
Expand Down Expand Up @@ -2023,6 +2029,15 @@ impl Wallet {
.filter(|local_output| {
params.bumping_fee.is_none() || local_output.chain_position.is_confirmed()
})
// Bitcoin Core's TRUC policy requires spending unconfirmed v3 outputs with a v3
// transaction.
.filter(|local_output| {
!excludes_unconfirmed_v3
|| local_output.chain_position.is_confirmed()
|| tx_graph
.get_tx(local_output.outpoint.txid)
.is_none_or(|tx| tx.version != transaction::Version(3))
})
.map(|utxo| WeightedUtxo {
satisfaction_weight: self
.public_descriptor(utxo.keychain)
Expand Down Expand Up @@ -2982,7 +2997,11 @@ mod test {
builder.add_utxo(outpoint).expect("should add local utxo");
let params = builder.params.clone();
// 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,
transaction::Version::TWO,
);
// Notice expected doesn't include the first output from two_output_tx as it should be
// filtered out.
let expected = vec![wallet
Expand Down
125 changes: 125 additions & 0 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,131 @@ fn test_create_tx_custom_version() {
assert_eq!(psbt.unsigned_tx.version.0, 42);
}

#[test]
fn test_create_tx_non_v3_excludes_unconfirmed_v3_utxos() {
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("wallet");

insert_checkpoint(
&mut wallet,
BlockId {
height: 1,
hash: BlockHash::all_zeros(),
},
);

let confirmed_tx = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
script_pubkey: wallet
.next_unused_address(KeychainKind::External)
.script_pubkey(),
value: Amount::from_sat(10_000),
}],
};
let confirmed_txid = confirmed_tx.compute_txid();
insert_tx(&mut wallet, confirmed_tx);
let block_id = wallet.latest_checkpoint().block_id();
insert_anchor(
&mut wallet,
confirmed_txid,
ConfirmationBlockTime {
block_id,
confirmation_time: 1,
},
);

let truc_tx = Transaction {
version: transaction::Version(3),
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
script_pubkey: wallet
.next_unused_address(KeychainKind::External)
.script_pubkey(),
value: Amount::from_sat(30_000),
}],
};
let truc_txid = truc_tx.compute_txid();
insert_tx(&mut wallet, truc_tx);

let recipient = wallet.next_unused_address(KeychainKind::External);

let mut builder = wallet.build_tx();
builder
.fee_rate(FeeRate::ZERO)
.add_recipient(recipient.script_pubkey(), Amount::from_sat(20_000));
assert_matches!(
builder.finish(),
Err(CreateTxError::CoinSelection(
coin_selection::InsufficientFunds { .. }
))
);

let mut builder = wallet.build_tx();
builder
.fee_rate(FeeRate::ZERO)
.version(3)
.add_recipient(recipient.script_pubkey(), Amount::from_sat(20_000));
let psbt = builder
.finish()
.expect("v3 transaction should be buildable");

assert_eq!(psbt.unsigned_tx.version, transaction::Version(3));
assert!(
psbt.unsigned_tx
.input
.iter()
.any(|input| input.previous_output.txid == truc_txid),
"version=3 transaction should be able to spend the unconfirmed v3 output"
);
}

#[test]
fn test_create_tx_non_v3_allows_unconfirmed_non_v3_utxos() {
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("wallet");

let unconfirmed_tx = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
script_pubkey: wallet
.next_unused_address(KeychainKind::External)
.script_pubkey(),
value: Amount::from_sat(30_000),
}],
};
let unconfirmed_txid = unconfirmed_tx.compute_txid();
insert_tx(&mut wallet, unconfirmed_tx);

let recipient = wallet.next_unused_address(KeychainKind::External);
let mut builder = wallet.build_tx();
builder
.fee_rate(FeeRate::ZERO)
.add_recipient(recipient.script_pubkey(), Amount::from_sat(20_000));
let psbt = builder
.finish()
.expect("non-v3 unconfirmed outputs should remain spendable");

assert!(
psbt.unsigned_tx
.input
.iter()
.any(|input| input.previous_output.txid == unconfirmed_txid),
"expected the unconfirmed non-v3 output to be available for selection"
);
}

#[test]
fn test_create_tx_default_locktime_is_last_sync_height() {
let (mut wallet, _) = get_funded_wallet_wpkh();
Expand Down