Skip to content

Commit 69adf1c

Browse files
committed
Gather utxo fragmentation stats after sim run
1 parent 5c6bd2a commit 69adf1c

File tree

2 files changed

+81
-1
lines changed

2 files changed

+81
-1
lines changed

src/lib.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,14 @@ pub struct SimulationResult {
718718
sim: Simulation,
719719
}
720720

721+
pub struct WalletUtxoStats {
722+
pub wallet_id: usize,
723+
pub dust_count: usize,
724+
pub total_count: usize,
725+
pub p50: Option<Amount>,
726+
pub p90: Option<Amount>,
727+
}
728+
721729
impl SimulationResult {
722730
pub fn new(sim: &Simulation) -> Self {
723731
Self {
@@ -793,6 +801,68 @@ impl SimulationResult {
793801
Amount::from_sat(total_fee_sat / counted)
794802
}
795803

804+
/// Count of dust UTXOs (<= 546 sats), computed from confirmed UTXOs.
805+
pub fn dust_utxo_count(&self) -> usize {
806+
const DUST_THRESHOLD_SATS: u64 = 546;
807+
self.sim
808+
.wallet_data
809+
.iter()
810+
.flat_map(|wallet| {
811+
let info = &self.sim.wallet_info[wallet.last_wallet_info_id.0];
812+
info.confirmed_utxos.iter()
813+
})
814+
.filter(|outpoint| {
815+
outpoint.with(&self.sim).data().amount.to_sat() <= DUST_THRESHOLD_SATS
816+
})
817+
.count()
818+
}
819+
820+
/// Sorted list of confirmed UTXO sizes, computed from current sim state.
821+
pub fn utxo_size_distribution(&self) -> Vec<Amount> {
822+
let mut amounts: Vec<Amount> = self
823+
.sim
824+
.wallet_data
825+
.iter()
826+
.flat_map(|wallet| {
827+
let info = &self.sim.wallet_info[wallet.last_wallet_info_id.0];
828+
info.confirmed_utxos.iter()
829+
})
830+
.map(|outpoint| outpoint.with(&self.sim).data().amount)
831+
.collect();
832+
amounts.sort_by_key(|amount| amount.to_sat());
833+
amounts
834+
}
835+
836+
/// Per-wallet dust counts and size percentiles (p50, p90) for confirmed UTXOs.
837+
pub fn wallet_utxo_stats(&self) -> Vec<WalletUtxoStats> {
838+
const DUST_THRESHOLD_SATS: u64 = 546;
839+
self.sim
840+
.wallet_data
841+
.iter()
842+
.map(|wallet| {
843+
let info = &self.sim.wallet_info[wallet.last_wallet_info_id.0];
844+
let mut amounts: Vec<Amount> = info
845+
.confirmed_utxos
846+
.iter()
847+
.map(|outpoint| outpoint.with(&self.sim).data().amount)
848+
.collect();
849+
amounts.sort_by_key(|amount| amount.to_sat());
850+
let dust_count = amounts
851+
.iter()
852+
.filter(|amount| amount.to_sat() <= DUST_THRESHOLD_SATS)
853+
.count();
854+
let total_count = amounts.len();
855+
WalletUtxoStats {
856+
wallet_id: wallet.id.0,
857+
dust_count,
858+
total_count,
859+
p50: percentile_amount(&amounts, 0.50),
860+
p90: percentile_amount(&amounts, 0.90),
861+
}
862+
})
863+
.collect()
864+
}
865+
796866
/// Save the transaction graph to a file.
797867
pub fn save_tx_graph(&self, path: impl AsRef<Path>) {
798868
let graph_svg = graphviz_rust::exec(
@@ -806,6 +876,16 @@ impl SimulationResult {
806876
// TODO: utxo fragmentation, anon set metrics
807877
}
808878

879+
fn percentile_amount(sorted: &[Amount], percentile: f64) -> Option<Amount> {
880+
if sorted.is_empty() {
881+
return None;
882+
}
883+
let n = sorted.len() as f64;
884+
let rank = (percentile * n).ceil().max(1.0);
885+
let idx = (rank as usize).saturating_sub(1);
886+
sorted.get(idx).copied()
887+
}
888+
809889
#[cfg(test)]
810890
mod tests {
811891
use bdk_coin_select::{Target, TargetFee, TargetOutputs};

src/transaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub(crate) struct Outpoint {
4848
}
4949

5050
impl<'a> Outpoint {
51-
fn with(&self, sim: &'a Simulation) -> OutputHandle<'a> {
51+
pub(crate) fn with(&self, sim: &'a Simulation) -> OutputHandle<'a> {
5252
OutputHandle {
5353
sim,
5454
outpoint: *self,

0 commit comments

Comments
 (0)