Skip to content

Commit 700b7de

Browse files
committed
Implement UIH1 AST node as TxOutSet → TxOutMask
UIH1 now takes Expr<TxOutSet> and returns Expr<TxOutMask>, matching the convention of other change heuristics. The underlying is_uih1_candidate was updated to take SpendableTxConstituent<T>, so the node wraps each output at the call site before delegating.
1 parent 148e020 commit 700b7de

2 files changed

Lines changed: 77 additions & 72 deletions

File tree

src/crates/heuristics/src/ast/uih.rs

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,57 @@
44
//! - UIH1 (Optimal change): smallest output is likely change when min(out) < min(in).
55
//! - UIH2 (Unnecessary input): transaction could pay outputs without the smallest input.
66
7-
use std::collections::HashSet;
7+
use std::collections::HashMap;
88

99
use tx_indexer_pipeline::{
1010
engine::EvalContext,
1111
expr::Expr,
1212
node::{Node, NodeId},
13-
value::{TxMask, TxOutSet, TxSet},
13+
value::{TxMask, TxOutMask, TxOutSet, TxSet},
14+
};
15+
use tx_indexer_primitives::{
16+
handle::SpendableTxConstituent,
17+
unified::{AnyOutId, AnyTxId},
1418
};
15-
use tx_indexer_primitives::unified::{AnyOutId, AnyTxId};
1619

1720
use crate::uih::UnnecessaryInputHeuristic;
1821

1922
/// Node that implements UIH1 (Optimal change heuristic).
2023
///
21-
/// For each transaction where min(output values) < min(input values), adds the
22-
/// smallest output(s) by value to the result set (likely change).
24+
/// For each output, returns `true` if its value is less than the minimum input
25+
/// value of its containing transaction.
2326
pub struct UnnecessaryInputHeuristic1Node {
24-
input: Expr<TxSet>,
27+
input: Expr<TxOutSet>,
2528
}
2629

2730
impl UnnecessaryInputHeuristic1Node {
28-
pub fn new(input: Expr<TxSet>) -> Self {
31+
pub fn new(input: Expr<TxOutSet>) -> Self {
2932
Self { input }
3033
}
3134
}
3235

3336
impl Node for UnnecessaryInputHeuristic1Node {
34-
type OutputValue = TxOutSet;
37+
type OutputValue = TxOutMask;
3538

3639
fn dependencies(&self) -> Vec<NodeId> {
3740
vec![self.input.id()]
3841
}
3942

40-
fn evaluate(&self, ctx: &EvalContext) -> HashSet<AnyOutId> {
41-
let tx_ids = ctx.get_or_default(&self.input);
42-
43-
let mut result = HashSet::new();
43+
fn evaluate(&self, ctx: &EvalContext) -> HashMap<AnyOutId, bool> {
44+
let txouts = ctx.get_or_default(&self.input);
45+
let mut result = HashMap::new();
4446

45-
for tx_id in &tx_ids {
46-
let tx = tx_id.with(ctx.unified_storage());
47-
48-
let outputs: Vec<_> = tx.outputs().map(|o| (o.id(), o.value())).collect();
49-
if outputs.is_empty() {
47+
for output_id in txouts.iter() {
48+
let Ok(spendable) =
49+
SpendableTxConstituent::try_new(output_id.with(ctx.unified_storage()))
50+
else {
51+
result.insert(*output_id, false);
5052
continue;
51-
}
52-
53-
if let Some(min_out) = UnnecessaryInputHeuristic::uih1_min_output_value(&tx) {
54-
for (out_id, v) in &outputs {
55-
if *v == min_out {
56-
result.insert(*out_id);
57-
}
58-
}
59-
}
53+
};
54+
result.insert(
55+
*output_id,
56+
UnnecessaryInputHeuristic::is_uih1_candidate(spendable),
57+
);
6058
}
6159

6260
result
@@ -71,9 +69,9 @@ impl Node for UnnecessaryInputHeuristic1Node {
7169
pub struct UnnecessaryInputHeuristic1;
7270

7371
impl UnnecessaryInputHeuristic1 {
74-
/// Returns the set of outputs that are the smallest by value in each tx
75-
/// where min(out) < min(in) (BlockSci optimal change heuristic).
76-
pub fn new(input: Expr<TxSet>) -> Expr<TxOutSet> {
72+
/// Returns a mask over outputs where `true` indicates a UIH1 candidate
73+
/// (output value < min input value of its containing transaction).
74+
pub fn new(input: Expr<TxOutSet>) -> Expr<TxOutMask> {
7775
let ctx = input.context().clone();
7876
ctx.register(UnnecessaryInputHeuristic1Node::new(input))
7977
}
@@ -246,15 +244,16 @@ mod tests {
246244
let mut engine = engine_with_loose(ctx.clone(), all_txs);
247245

248246
let source = AllLooseTxs::new(&ctx);
249-
let uih1 = UnnecessaryInputHeuristic1::new(source.txs());
247+
let uih1 = UnnecessaryInputHeuristic1::new(source.txs().outputs());
250248
let result = engine.eval(&uih1);
251249

252250
let smallest_out = AnyOutId::from(TxOutId::new(TxId(3), 0));
253-
assert!(
254-
result.contains(&smallest_out),
255-
"UIH1 should contain the smallest output (value 50)"
251+
assert_eq!(
252+
result.get(&smallest_out),
253+
Some(&true),
254+
"UIH1 should flag the smallest output (value 50)"
256255
);
257-
assert_eq!(result.len(), 1);
256+
assert_eq!(result.values().filter(|&&v| v).count(), 1);
258257
}
259258

260259
#[test]
@@ -264,12 +263,12 @@ mod tests {
264263
let mut engine = engine_with_loose(ctx.clone(), all_txs);
265264

266265
let source = AllLooseTxs::new(&ctx);
267-
let uih1 = UnnecessaryInputHeuristic1::new(source.txs());
266+
let uih1 = UnnecessaryInputHeuristic1::new(source.txs().outputs());
268267
let result = engine.eval(&uih1);
269268

270269
assert!(
271-
result.is_empty(),
272-
"UIH1 should be empty when min(out) >= min(in)"
270+
result.values().all(|&v| !v),
271+
"UIH1 should have no candidates when min(out) >= min(in)"
273272
);
274273
}
275274

@@ -280,12 +279,18 @@ mod tests {
280279
let mut engine = engine_with_loose(ctx.clone(), all_txs);
281280

282281
let source = AllLooseTxs::new(&ctx);
283-
let uih1 = UnnecessaryInputHeuristic1::new(source.txs());
282+
let uih1 = UnnecessaryInputHeuristic1::new(source.txs().outputs());
284283
let result = engine.eval(&uih1);
285284

286-
assert!(result.contains(&AnyOutId::from(TxOutId::new(TxId(3), 0))));
287-
assert!(result.contains(&AnyOutId::from(TxOutId::new(TxId(3), 1))));
288-
assert_eq!(result.len(), 2);
285+
assert_eq!(
286+
result.get(&AnyOutId::from(TxOutId::new(TxId(3), 0))),
287+
Some(&true)
288+
);
289+
assert_eq!(
290+
result.get(&AnyOutId::from(TxOutId::new(TxId(3), 1))),
291+
Some(&true)
292+
);
293+
assert_eq!(result.values().filter(|&&v| v).count(), 2);
289294
}
290295

291296
#[test]
@@ -367,21 +372,23 @@ mod tests {
367372
let mut engine = engine_with_loose(ctx.clone(), all_txs);
368373

369374
let source = AllLooseTxs::new(&ctx);
370-
let uih1 = UnnecessaryInputHeuristic1::new(source.txs());
375+
let uih1 = UnnecessaryInputHeuristic1::new(source.txs().outputs());
371376
let uih2 = UnnecessaryInputHeuristic2::new(source.txs());
372377

373378
// `.into_owned()` because we hold both results simultaneously below;
374379
// each `eval` borrows the engine, so we drop the borrows by cloning out.
375380
let uih1_result = engine.eval(&uih1).into_owned();
376381
let uih2_result = engine.eval(&uih2).into_owned();
377382

378-
assert!(
379-
uih1_result.contains(&AnyOutId::from(TxOutId::new(TxId(5), 1))),
383+
assert_eq!(
384+
uih1_result.get(&AnyOutId::from(TxOutId::new(TxId(5), 1))),
385+
Some(&true),
380386
"UIH1 should flag tx4's smallest output (vout=1)"
381387
);
382-
assert!(
383-
!uih1_result.contains(&AnyOutId::from(TxOutId::new(TxId(6), 0))),
384-
"UIH1 should not flag tx5 (min(out) >= min(in))"
388+
assert_ne!(
389+
uih1_result.get(&AnyOutId::from(TxOutId::new(TxId(6), 0))),
390+
Some(&true),
391+
"UIH1 should not flag tx5's vout=0 (min(out) >= min(in))"
385392
);
386393

387394
// uih2: tx4 true, tx5 false

src/crates/heuristics/src/uih.rs

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
11
use bitcoin::Amount;
2-
use tx_indexer_primitives::traits::abstract_types::{
3-
EnumerateInputValueInArbitraryOrder, EnumerateOutputValueInArbitraryOrder,
2+
use tx_indexer_primitives::{
3+
AbstractTransaction,
4+
handle::SpendableTxConstituent,
5+
traits::abstract_types::{
6+
EnumerateInputValueInArbitraryOrder, EnumerateOutputValueInArbitraryOrder, TxConstituent,
7+
},
48
};
59

610
pub struct UnnecessaryInputHeuristic;
711

812
impl UnnecessaryInputHeuristic {
9-
pub fn uih1_min_output_value<T>(tx: &T) -> Option<Amount>
13+
/// Returns the minimum output value that is smaller than the minimum input value.
14+
pub fn is_uih1_candidate<T>(txout: SpendableTxConstituent<T>) -> bool
1015
where
11-
T: EnumerateInputValueInArbitraryOrder + EnumerateOutputValueInArbitraryOrder,
16+
T: TxConstituent<
17+
Handle: EnumerateInputValueInArbitraryOrder + EnumerateOutputValueInArbitraryOrder,
18+
>,
1219
{
13-
let input_values: Vec<Amount> = tx.input_values().collect();
14-
let output_values: Vec<Amount> = tx.output_values().collect();
15-
16-
if input_values.is_empty() || output_values.is_empty() {
17-
return None;
20+
let tx = txout.containing_tx();
21+
let min_in = tx.input_values().min();
22+
let output_val = tx
23+
.output_at(txout.vout())
24+
.expect("vout should be present")
25+
.value();
26+
27+
if let Some(min_in) = min_in
28+
&& output_val < min_in
29+
{
30+
return true;
1831
}
1932

20-
let min_in = input_values
21-
.iter()
22-
.min()
23-
.copied()
24-
.expect("non-empty inputs");
25-
let min_out = output_values
26-
.iter()
27-
.min()
28-
.copied()
29-
.expect("non-empty outputs");
30-
31-
if min_out < min_in {
32-
Some(min_out)
33-
} else {
34-
None
35-
}
33+
false
3634
}
3735

3836
pub fn is_uih2<T>(tx: &T) -> bool

0 commit comments

Comments
 (0)