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
9 changes: 6 additions & 3 deletions ci/pin-msrv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ cargo update -p tracing-core --precise "0.1.33"
cargo update -p "webpki-roots@1.0.6" --precise "1.0.1"
cargo update -p rayon --precise "1.10.0"
cargo update -p rayon-core --precise "1.12.1"
cargo update -p quote --precise "1.0.41"
cargo update -p syn --precise "2.0.106"
cargo update -p openssl --precise "0.10.73"
cargo update -p openssl-sys --precise "0.9.109"
cargo update -p "getrandom@0.4.2" --precise "0.2.17"
Expand All @@ -48,7 +46,6 @@ cargo update -p futures-core --precise "0.3.31"
cargo update -p futures-io --precise "0.3.31"
cargo update -p futures-sink --precise "0.3.31"
cargo update -p futures-task --precise "0.3.31"
cargo update -p proc-macro2 --precise "1.0.92"
cargo update -p log --precise "0.4.22"
cargo update -p itoa --precise "1.0.11"
cargo update -p anyhow --precise "1.0.86"
Expand All @@ -57,3 +54,9 @@ cargo update -p hyper-util --precise "0.1.6"
cargo update -p pin-project --precise "1.1.5"
cargo update -p pin-project-internal --precise "1.1.5"
cargo update -p "rustls@0.23.37" --precise "0.23.26"

# all pinning required due to `clap` usage in `example_cli`
cargo update -p "clap@4.6.0" --precise "4.5.17"
cargo update -p proc-macro2 --precise "1.0.92"
cargo update -p quote --precise "1.0.41"
cargo update -p syn --precise "2.0.106"
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 @@ -415,6 +416,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, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, 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 @@ -315,6 +315,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
/// 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 @@ -364,3 +466,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
Loading