consolidation_tests.rs (8868B)
1 use std::collections::{BTreeMap, BTreeSet}; 2 3 use super::{AddressNetwork, Ledger, OutPoint, TransactionV2, TxOutput, UtxoLineageRoot, Wallet}; 4 use crate::app::NodeCore; 5 6 fn fixture(count: usize, value: u64, roots: bool) -> (Wallet, Ledger) { 7 let wallet = Wallet::from_seed("consolidation-tests"); 8 let mut ledger = Ledger::new(BTreeMap::new(), 1); 9 for index in 0..count { 10 let point = OutPoint { 11 txid: format!("{:064x}", index + 1), 12 index: 0, 13 }; 14 ledger.utxos.insert( 15 point.clone(), 16 TxOutput { 17 address: wallet.address().to_string(), 18 amount: value, 19 }, 20 ); 21 if roots { 22 ledger.utxo_lineage.insert( 23 point.clone(), 24 UtxoLineageRoot { 25 outpoint: point, 26 height: 0, 27 }, 28 ); 29 } 30 } 31 (wallet, ledger) 32 } 33 34 #[test] 35 fn consolidation_large_wallet_is_bounded_disjoint_and_keeps_a_reserve() { 36 let (wallet, ledger) = fixture(1247, 1_000_000, false); 37 let mut node = NodeCore::from_ledger(wallet.clone(), ledger, 0); 38 let plan = node.consolidation_plan(1, false).unwrap(); 39 assert_eq!(plan.before, 1247); 40 assert_eq!(plan.batches.len(), 10); 41 assert_eq!(plan.after, 11); 42 let mut selected = BTreeSet::new(); 43 for batch in &plan.batches { 44 assert!((2..=128).contains(&batch.utxos.len())); 45 for point in &batch.utxos { 46 assert!(selected.insert(point.clone())); 47 } 48 assert!(batch.fee >= batch.bytes as u64); 49 let tx = node 50 .consolidate(&batch.utxos, 1, batch.fee, false, wallet.address()) 51 .unwrap(); 52 assert!(!tx.is_empty()); 53 let pending = node 54 .pending_transactions() 55 .into_iter() 56 .find(|transaction| transaction.signature() == tx) 57 .unwrap(); 58 let outputs = pending.outputs(); 59 assert_eq!(outputs.len(), 1); 60 assert_eq!(outputs[0].address, wallet.address()); 61 assert_eq!( 62 outputs[0].amount + pending.fee(), 63 batch.utxos.len() as u64 * 1_000_000 64 ); 65 } 66 assert_eq!(selected.len(), 1246); 67 assert_eq!( 68 plan.fee, 69 plan.batches.iter().map(|batch| batch.fee).sum::<u64>() 70 ); 71 let batch = &plan.batches[0]; 72 assert!( 73 node.consolidate(&batch.utxos, 1, batch.fee, false, wallet.address()) 74 .is_err() 75 ); 76 } 77 78 #[test] 79 fn consolidation_plan_includes_hybrid_outputs_in_separate_v2_batches() { 80 let (wallet, mut ledger) = fixture(3, 1_000_000, false); 81 let hybrid_address = wallet.hybrid_address(AddressNetwork::Mainnet); 82 for index in 0..3 { 83 ledger.utxos.insert( 84 OutPoint { 85 txid: format!("{:064x}", index + 10), 86 index: 0, 87 }, 88 TxOutput { 89 address: hybrid_address.clone(), 90 amount: 2_000_000, 91 }, 92 ); 93 } 94 95 let node = NodeCore::from_ledger(wallet, ledger, 0); 96 let plan = node.consolidation_plan(1, true).unwrap(); 97 assert_eq!(plan.before, 6); 98 assert_eq!(plan.after, 4); 99 assert_eq!(plan.batches.len(), 2); 100 assert_eq!( 101 plan.batches 102 .iter() 103 .filter(|batch| batch.kind == crate::app::ConsolidationKind::Legacy) 104 .count(), 105 1 106 ); 107 assert_eq!( 108 plan.batches 109 .iter() 110 .filter(|batch| batch.kind == crate::app::ConsolidationKind::Hybrid) 111 .count(), 112 1 113 ); 114 } 115 116 #[test] 117 fn selected_hybrid_outputs_build_one_v2_consolidation_output() { 118 let wallet = Wallet::from_seed("hybrid-consolidation-builder"); 119 let address = wallet.hybrid_address(AddressNetwork::Mainnet); 120 let points = (0..2) 121 .map(|index| OutPoint { 122 txid: format!("{:064x}", index + 1), 123 index: 0, 124 }) 125 .collect::<Vec<_>>(); 126 let mut ledger = Ledger::new(BTreeMap::new(), 1); 127 for point in &points { 128 ledger.utxos.insert( 129 point.clone(), 130 TxOutput { 131 address: address.clone(), 132 amount: 1_000_000, 133 }, 134 ); 135 } 136 137 let transaction = ledger 138 .build_v2_consolidation_with_inputs(&wallet, 1_990_000, 10_000, &points) 139 .unwrap(); 140 let TransactionV2::Transfer { 141 inputs, 142 outputs, 143 fee, 144 .. 145 } = transaction 146 else { 147 panic!("expected a transaction-v2 transfer"); 148 }; 149 assert_eq!(inputs.len(), 2); 150 assert_eq!(outputs.len(), 1); 151 assert_eq!(outputs[0].address, wallet.hybrid_versioned_address()); 152 assert_eq!(outputs[0].amount, 1_990_000); 153 assert_eq!(fee, 10_000); 154 } 155 156 #[test] 157 fn consolidation_plan_excludes_outputs_reserved_by_pending_v2() { 158 let wallet = Wallet::from_seed("pending-v2-consolidation"); 159 let address = wallet.hybrid_address(AddressNetwork::Mainnet); 160 let points = (0..3) 161 .map(|index| OutPoint { 162 txid: format!("{:064x}", index + 1), 163 index: 0, 164 }) 165 .collect::<Vec<_>>(); 166 let mut ledger = Ledger::new(BTreeMap::new(), 1); 167 for point in &points { 168 ledger.utxos.insert( 169 point.clone(), 170 TxOutput { 171 address: address.clone(), 172 amount: 1_000_000, 173 }, 174 ); 175 } 176 let pending = ledger 177 .build_v2_consolidation_with_inputs(&wallet, 1_990_000, 10_000, &points[..2]) 178 .unwrap(); 179 ledger.pending_v2.push(pending); 180 181 let node = NodeCore::from_ledger(wallet, ledger, 0); 182 let plan = node.consolidation_plan(1, true).unwrap(); 183 assert_eq!(plan.before, 3); 184 assert_eq!(plan.after, 3); 185 assert!(plan.batches.is_empty()); 186 } 187 188 #[test] 189 fn consolidation_requires_consent_for_distinct_mining_roots_and_respects_fee_cap() { 190 let (wallet, ledger) = fixture(4, 1_000_000, true); 191 let mut node = NodeCore::from_ledger(wallet.clone(), ledger, 0); 192 assert!( 193 node.consolidation_plan(1, false) 194 .unwrap() 195 .batches 196 .is_empty() 197 ); 198 let plan = node.consolidation_plan(1, true).unwrap(); 199 let batch = &plan.batches[0]; 200 assert!( 201 node.consolidate(&batch.utxos, 1, batch.fee, false, wallet.address()) 202 .is_err() 203 ); 204 assert!( 205 node.consolidate(&batch.utxos, 1, batch.fee - 1, true, wallet.address()) 206 .is_err() 207 ); 208 assert!( 209 node.consolidate(&batch.utxos, 1, batch.fee, true, "another-wallet") 210 .is_err() 211 ); 212 assert!( 213 node.consolidate( 214 &[batch.utxos[0].clone(), batch.utxos[0].clone()], 215 1, 216 batch.fee, 217 true, 218 wallet.address() 219 ) 220 .is_err() 221 ); 222 } 223 224 #[test] 225 fn consolidation_skips_uneconomic_and_locked_wallets() { 226 let (wallet, ledger) = fixture(3, 10, false); 227 let node = NodeCore::from_ledger(wallet.clone(), ledger.clone(), 0); 228 assert!( 229 node.consolidation_plan(1, false) 230 .unwrap() 231 .batches 232 .is_empty() 233 ); 234 let locked = NodeCore::from_locked_wallet_address(wallet.address(), ledger, false, 0, 1); 235 assert!(locked.consolidation_plan(1, false).is_err()); 236 } 237 238 #[test] 239 fn consolidation_large_uneconomic_wallet_skips_dust_without_building_transactions() { 240 let (wallet, ledger) = fixture(2000, 10, false); 241 let node = NodeCore::from_ledger(wallet, ledger, 0); 242 let plan = node.consolidation_plan(1, false).unwrap(); 243 assert_eq!(plan.before, 2000); 244 assert_eq!(plan.after, 2000); 245 assert!(plan.batches.is_empty()); 246 } 247 248 #[test] 249 fn consolidation_adapts_to_the_network_block_limit() { 250 let (wallet, mut ledger) = fixture(150, 1_000_000, false); 251 ledger.launch_profile.max_block_bytes = 3000; 252 let node = NodeCore::from_ledger(wallet, ledger, 0); 253 let plan = node.consolidation_plan(1, false).unwrap(); 254 assert!(!plan.batches.is_empty()); 255 assert!(plan.batches.iter().all(|batch| batch.utxos.len() < 128)); 256 } 257 258 #[test] 259 fn transfer_selection_uses_best_fit_then_largest_inputs() { 260 let (wallet, mut ledger) = fixture(4, 10, false); 261 ledger 262 .utxos 263 .values_mut() 264 .zip([10, 100, 1000, 200]) 265 .for_each(|(output, amount)| output.amount = amount); 266 let single = ledger 267 .build_transfer(&wallet, wallet.address(), 99, 1) 268 .unwrap(); 269 assert_eq!(single.inputs().len(), 1); 270 assert_eq!(ledger.utxos[&single.inputs()[0].outpoint].amount, 100); 271 let multiple = ledger 272 .build_transfer(&wallet, wallet.address(), 1100, 1) 273 .unwrap(); 274 assert_eq!(multiple.inputs().len(), 2); 275 assert_eq!(ledger.utxos[&multiple.inputs()[0].outpoint].amount, 1000); 276 assert_eq!(ledger.utxos[&multiple.inputs()[1].outpoint].amount, 200); 277 }