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
99use 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
1720use 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 .
2326pub struct UnnecessaryInputHeuristic1Node {
24- input : Expr < TxSet > ,
27+ input : Expr < TxOutSet > ,
2528}
2629
2730impl UnnecessaryInputHeuristic1Node {
28- pub fn new ( input : Expr < TxSet > ) -> Self {
31+ pub fn new ( input : Expr < TxOutSet > ) -> Self {
2932 Self { input }
3033 }
3134}
3235
3336impl 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 {
7169pub struct UnnecessaryInputHeuristic1 ;
7270
7371impl 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
0 commit comments