iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }