Skip to content

Commit 0c6c1cc

Browse files
committed
fix(wallet): exclude unconfirmed v3 outputs from non-v3 coin selection
When a v3 (TRUC) transaction produced change back into the wallet, that unconfirmed output entered coin selection like any other UTXO. Building a subsequent non-v3 transaction that spent it caused bitcoind to reject the broadcast with "TRUC-violation, non-version=3 tx cannot spend from version=3 tx" (BIP-431). Thread `tx_version` into `filter_utxos` so that unconfirmed UTXOs whose parent is version 3 are excluded when building a non-v3 transaction. Confirmed outputs are unaffected since TRUC rules only apply while the parent remains unconfirmed. Fixes #419.
1 parent 5df32ca commit 0c6c1cc

1 file changed

Lines changed: 24 additions & 5 deletions

File tree

src/wallet/mod.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,7 +1429,7 @@ impl Wallet {
14291429
let (required_utxos, optional_utxos) = {
14301430
// NOTE: manual selection overrides unspendable
14311431
let mut required: Vec<WeightedUtxo> = params.utxos.clone();
1432-
let optional = self.filter_utxos(&params, current_height.to_consensus_u32());
1432+
let optional = self.filter_utxos(&params, current_height.to_consensus_u32(), version);
14331433

14341434
// If `drain_wallet` is true, all UTxOs are required.
14351435
if params.drain_wallet {
@@ -1978,18 +1978,24 @@ impl Wallet {
19781978

19791979
/// Given the options returns the list of utxos that must be used to form the
19801980
/// transaction and any further that may be used if needed.
1981-
fn filter_utxos(&self, params: &TxParams, current_height: u32) -> Vec<WeightedUtxo> {
1981+
fn filter_utxos(
1982+
&self,
1983+
params: &TxParams,
1984+
current_height: u32,
1985+
tx_version: transaction::Version,
1986+
) -> Vec<WeightedUtxo> {
19821987
if params.manually_selected_only {
19831988
vec![]
19841989
// Only process optional UTxOs if manually_selected_only is false.
19851990
} else {
1991+
let tx_graph = self.tx_graph.graph();
1992+
let excludes_unconfirmed_v3 = tx_version != transaction::Version(3);
19861993
let manually_selected_outpoints = params
19871994
.utxos
19881995
.iter()
19891996
.map(|wutxo| wutxo.utxo.outpoint())
19901997
.collect::<HashSet<OutPoint>>();
1991-
self.tx_graph
1992-
.graph()
1998+
tx_graph
19931999
// Get all unspent UTxOs from wallet.
19942000
// NOTE: the UTxOs returned by the following method already belong to wallet as the
19952001
// call chain uses get_tx_node infallibly.
@@ -2023,6 +2029,15 @@ impl Wallet {
20232029
.filter(|local_output| {
20242030
params.bumping_fee.is_none() || local_output.chain_position.is_confirmed()
20252031
})
2032+
// Bitcoin Core's TRUC policy requires spending unconfirmed v3 outputs with a v3
2033+
// transaction.
2034+
.filter(|local_output| {
2035+
!excludes_unconfirmed_v3
2036+
|| local_output.chain_position.is_confirmed()
2037+
|| tx_graph
2038+
.get_tx(local_output.outpoint.txid)
2039+
.is_none_or(|tx| tx.version != transaction::Version(3))
2040+
})
20262041
.map(|utxo| WeightedUtxo {
20272042
satisfaction_weight: self
20282043
.public_descriptor(utxo.keychain)
@@ -2982,7 +2997,11 @@ mod test {
29822997
builder.add_utxo(outpoint).expect("should add local utxo");
29832998
let params = builder.params.clone();
29842999
// enforce selection of first output in transaction
2985-
let received = wallet.filter_utxos(&params, wallet.latest_checkpoint().block_id().height);
3000+
let received = wallet.filter_utxos(
3001+
&params,
3002+
wallet.latest_checkpoint().block_id().height,
3003+
transaction::Version::TWO,
3004+
);
29863005
// Notice expected doesn't include the first output from two_output_tx as it should be
29873006
// filtered out.
29883007
let expected = vec![wallet

0 commit comments

Comments
 (0)