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
22 changes: 22 additions & 0 deletions crates/chain/src/indexer/keychain_txout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use core::{
ops::{Bound, RangeBounds},
};

use crate::spk_txout::{CreatedTxOut, SpentTxOut};
use crate::Merge;

/// The default lookahead for a [`KeychainTxOutIndex`]
Expand Down Expand Up @@ -418,6 +419,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
.sent_and_received(tx, self.map_to_inner_bounds(range))
}

/// Returns the [`SpentTxOut`]s for the `tx` relative to the script pubkeys belonging to the
/// keychain. A TxOut is *spent* when a keychain script pubkey is in any input. For
/// `spent_txouts` to be computed correctly, the index must have already scanned the output
/// being spent.
pub fn spent_txouts<'a>(
&'a self,
tx: &'a Transaction,
) -> impl Iterator<Item = SpentTxOut<(K, u32)>> + 'a {
self.inner.spent_txouts(tx)
}

/// Returns the [`CreatedTxOut`]s for the `tx` relative to the script pubkeys
/// belonging to the keychain. A TxOut is *created* when it is on an output.
/// These are computed directly from the transaction outputs.
pub fn created_txouts<'a>(
&'a self,
tx: &'a Transaction,
) -> impl Iterator<Item = CreatedTxOut<(K, u32)>> + 'a {
self.inner.created_txouts(tx)
}

/// Computes the net value that this transaction gives to the script pubkeys in the index and
/// *takes* from the transaction outputs in the index. Shorthand for calling
/// [`sent_and_received`] and subtracting sent from received.
Expand Down
139 changes: 138 additions & 1 deletion crates/chain/src/indexer/spk_txout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
Indexer,
};
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid};

/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
///
Expand Down Expand Up @@ -318,6 +318,108 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
(sent, received)
}

/// Returns the relevant [`SpentTxOut`]s for a [`Transaction`]
///
/// TxOuts are *spent* when an indexed script pubkey is found in one of the transaction's
/// inputs. For these to be computed correctly, the index must have already scanned the
/// output being spent.
///
/// # Example
/// Shows the addresses of the TxOut spent from a Transaction relevant to spks in this index.
///
/// ```rust
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
/// # use bitcoin::{Address, Network, Transaction};
/// # use std::str::FromStr;
/// #
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let mut index = SpkTxOutIndex::<u32>::default();
///
/// // ... scan transactions to populate the index ...
/// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
///
/// // Get spent txouts for a transaction for all indexed spks
/// let spent_txouts = index.spent_txouts(&tx);
///
/// // Display addresses and amounts
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tnull would this information be useful for LDKnode when handling the transactions in WalletEvents ?

Copy link
Contributor

@tnull tnull Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, that would be very helpful, we just recently got requests to be able to see which address was sent to: lightningdevkit/ldk-node#684

Do you happen to see a way to include the output's index in this API, too? (cf. lightningdevkit/ldk-node#717). Maybe the return type could be (Vec<(usize, TxOut)>, Vec<(usize, TxOut)>), or a similar struct representation?

Copy link
Member Author

@notmandatory notmandatory Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, since the input & output Vecs of Transaction are in the correct order I'm able to return the index info with the TxOuts. See 6ff9f26.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tnull did you get a chance to look at this again? it now returns the index and TxOut as you suggested.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thank you! Looks good!

/// println!("Spent:");
/// for spent in spent_txouts {
/// let address = Address::from_script(&spent.txout.script_pubkey, Network::Bitcoin)?;
/// println!("input {}: from {} - {}", spent.outpoint().vout, address, &spent.txout.value.to_sat());
/// }
/// # Ok(())
/// # }
/// ```
pub fn spent_txouts<'a>(
&'a self,
tx: &'a Transaction,
) -> impl Iterator<Item = SpentTxOut<I>> + 'a {
tx.input
.iter()
.enumerate()
.filter_map(|(input_index, txin)| {
self.txout(txin.previous_output)
.map(|(index, txout)| SpentTxOut {
txout: txout.clone(),
spending_input: txin.clone(),
spending_input_index: u32::try_from(input_index)
.expect("invalid input index"),
spk_index: index.clone(),
})
})
}

/// Returns the relevant [`CreatedTxOut`]s for a [`Transaction`]
///
/// TxOuts are *created* when an indexed script pubkey is found in one of the transaction's
/// outputs. These are computed directly from the transaction outputs.
///
/// # Example
/// Shows the addresses of the TxOut created by a Transaction relevant to spks in this index.
///
/// ```rust
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
/// # use bitcoin::{Address, Network, Transaction};
/// # use std::str::FromStr;
/// #
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let mut index = SpkTxOutIndex::<u32>::default();
///
/// // ... scan transactions to populate the index ...
/// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
///
/// // Get created txouts for a transaction for all indexed spks
/// let created_txouts = index.created_txouts(&tx);
///
/// // Display addresses and amounts
/// println!("Created:");
/// for created in created_txouts {
/// let address = Address::from_script(&created.txout.script_pubkey, Network::Bitcoin)?;
/// println!("output {}: to {} + {}", &created.outpoint.vout, address, &created.txout.value.display_dynamic());
/// }
/// # Ok(())
/// # }
/// ```
pub fn created_txouts<'a>(
&'a self,
tx: &'a Transaction,
) -> impl Iterator<Item = CreatedTxOut<I>> + 'a {
tx.output
.iter()
.enumerate()
.filter_map(|(output_index, txout)| {
self.index_of_spk(txout.script_pubkey.clone())
.map(|index| CreatedTxOut {
outpoint: OutPoint {
txid: tx.compute_txid(),
vout: u32::try_from(output_index).expect("invalid output index"),
},
txout: txout.clone(),
spk_index: index.clone(),
})
})
}

/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
/// for calling [`sent_and_received`] and subtracting sent from received.
///
Expand Down Expand Up @@ -367,3 +469,38 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
spks_from_inputs.chain(spks_from_outputs).collect()
}
}

/// A transaction output that was spent by a transaction input.
///
/// Contains information about the spent output and the input that spent it.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct SpentTxOut<I> {
/// The transaction output that was spent.
pub txout: TxOut,
/// The transaction input that spent the output.
pub spending_input: TxIn,
/// The index of the spending input in the transaction.
pub spending_input_index: u32,
/// The script pubkey index associated with the spent output.
pub spk_index: I,
}

impl<I> SpentTxOut<I> {
/// Returns the outpoint of the spent transaction output.
pub fn outpoint(&self) -> OutPoint {
self.spending_input.previous_output
}
}

/// A transaction output that was created by a transaction.
///
/// Contains information about the created output and its location.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct CreatedTxOut<I> {
/// The outpoint identifying the created output.
pub outpoint: OutPoint,
/// The transaction output that was created.
pub txout: TxOut,
/// The script pubkey index associated with the created output.
pub spk_index: I,
}
116 changes: 116 additions & 0 deletions crates/chain/tests/test_spk_txout_index.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use bdk_chain::spk_txout::{CreatedTxOut, SpentTxOut};
use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
Expand Down Expand Up @@ -80,6 +81,121 @@ fn spk_txout_sent_and_received() {
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
}

#[test]
fn spk_txout_spent_created_txouts() {
let spk0 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
let spk1 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();

let mut index = SpkTxOutIndex::default();
index.insert_spk(0, spk0.clone());
index.insert_spk(1, spk1.clone());

let tx1 = Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::from_sat(42_000),
script_pubkey: spk0.clone(),
}],
};
index.scan(&tx1);
let spent_txouts = index.spent_txouts(&tx1).collect::<Vec<_>>();
assert!(spent_txouts.is_empty());

let created_txouts = index.created_txouts(&tx1).collect::<Vec<_>>();
assert_eq!(created_txouts.len(), 1);
assert_eq!(
created_txouts[0],
CreatedTxOut {
outpoint: OutPoint {
txid: tx1.compute_txid(),
vout: 0,
},
txout: TxOut {
value: Amount::from_sat(42_000),
script_pubkey: spk0.clone(),
},
spk_index: 0,
}
);

let tx2 = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: tx1.compute_txid(),
vout: 0,
},
..Default::default()
}],
output: vec![
TxOut {
value: Amount::from_sat(20_000),
script_pubkey: spk1.clone(),
},
TxOut {
script_pubkey: spk0.clone(),
value: Amount::from_sat(30_000),
},
],
};
index.scan(&tx2);

let spent_txouts = index.spent_txouts(&tx2).collect::<Vec<_>>();
assert_eq!(spent_txouts.len(), 1);
assert_eq!(
spent_txouts[0],
SpentTxOut {
txout: TxOut {
value: Amount::from_sat(42_000),
script_pubkey: spk0.clone(),
},
spending_input: TxIn {
previous_output: OutPoint {
txid: tx1.compute_txid(),
vout: 0,
},
..Default::default()
},
spending_input_index: 0,
spk_index: 0,
}
);

let created_txouts = index.created_txouts(&tx2).collect::<Vec<_>>();
assert_eq!(created_txouts.len(), 2);
assert_eq!(
created_txouts[0],
CreatedTxOut {
outpoint: OutPoint {
txid: tx2.compute_txid(),
vout: 0,
},
txout: TxOut {
value: Amount::from_sat(20_000),
script_pubkey: spk1.clone(),
},
spk_index: 1,
}
);
assert_eq!(
created_txouts[1],
CreatedTxOut {
outpoint: OutPoint {
txid: tx2.compute_txid(),
vout: 1,
},
txout: TxOut {
value: Amount::from_sat(30_000),
script_pubkey: spk0.clone(),
},
spk_index: 0,
}
);
}

#[test]
fn mark_used() {
let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
Expand Down