iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

tests.rs (19292B)


      1 use std::collections::BTreeMap;
      2 
      3 use crate::{
      4     adapters::config_store::{DEFAULT_POW_MINING_WORKERS, MAX_POW_MINING_WORKERS},
      5     app::{GossipEnvelope, NodeConfig, NodeCore},
      6     domain::{
      7         FinalizerMode, GenesisBurn, Ledger, MICRO_IUNA, MINE_ACTIONS_PER_ANCHOR_LIMIT,
      8         MINE_FINALIZER_FEE, RECOVERY_BLOCK_DELAY_MS, Transaction, VDF_TARGET_BLOCK_MS, Wallet,
      9         run_vdf,
     10     },
     11 };
     12 
     13 #[test]
     14 fn same_height_verified_import_does_not_reset_auto_burn_guard() {
     15     let alice = Wallet::from_seed("same-height-import-alice");
     16     let mut allocations = BTreeMap::new();
     17     allocations.insert(alice.address().to_string(), MICRO_IUNA);
     18     let mut node = NodeCore::new(NodeConfig {
     19         wallet: alice,
     20         genesis_allocations: allocations,
     21         vdf_rounds: 10,
     22         burn_per_block: 1,
     23         burn_fee: 1,
     24         pow_mining_workers: 1,
     25         recovery_vdf_top_rank_percent: 100,
     26     });
     27 
     28     let first = node.prepare_automatic_mining(1);
     29     assert!(first.burned.is_some());
     30     assert_eq!(node.last_auto_burn_height, Some(0));
     31 
     32     let same_height_ledger = node.clone_ledger();
     33     assert!(!node.import_verified_ledger(same_height_ledger).unwrap());
     34     assert_eq!(node.last_auto_burn_height, Some(0));
     35 
     36     let second = node.prepare_automatic_mining(2);
     37     assert!(second.burned.is_none());
     38 }
     39 
     40 #[test]
     41 fn automatic_pow_mining_searches_bounded_nonce_batches_per_tip() {
     42     let wallet = Wallet::from_seed("automatic-pow-mining-wallet");
     43     let ledger = Ledger::new_with_genesis_burns(
     44         BTreeMap::from([(wallet.address().to_string(), 1)]),
     45         vec![GenesisBurn::new(wallet.address(), 1)],
     46         10,
     47     )
     48     .unwrap();
     49     let mut node = NodeCore::from_ledger(wallet.clone(), ledger, 0);
     50 
     51     let disabled = node.prepare_automatic_mining(1);
     52     assert!(disabled.pow_mined.is_none());
     53     assert_eq!(
     54         disabled.skipped_reason.as_deref(),
     55         Some("automatic mining is off")
     56     );
     57 
     58     node.set_pow_mining_enabled(true);
     59     let first = node.prepare_automatic_mining(2);
     60     assert!(node.ledger().pending_blinded_transactions().len() <= 1);
     61     let first = std::iter::once(first)
     62         .chain((3..10_000).map(|timestamp| node.prepare_automatic_mining(timestamp)))
     63         .find(|plan| plan.pow_mined.is_some())
     64         .expect("bounded PoW search should eventually find a proof");
     65     let first_mine = first.pow_mined.as_ref().expect("PoW should be queued");
     66     let Transaction::Mine {
     67         anchor,
     68         recipient,
     69         difficulty_bits,
     70         ..
     71     } = first_mine
     72     else {
     73         panic!("expected mine transaction");
     74     };
     75     assert_eq!(anchor, &node.chain().last().unwrap().hash);
     76     assert_eq!(recipient, wallet.address());
     77     assert_eq!(
     78         *difficulty_bits,
     79         node.ledger().current_mine_difficulty_bits()
     80     );
     81     let first_pending = node.ledger().pending().len();
     82     assert!(first_pending >= 1);
     83     assert!(node.ledger().pending_blinded_transactions().is_empty());
     84     assert!(node.drain_outbox().iter().any(|envelope| {
     85             matches!(envelope, GossipEnvelope::MineAction(tx) if tx.signature() == first_mine.signature())
     86         }));
     87     assert!(
     88         node.status()
     89             .mining
     90             .last_auto_pow_mine_status
     91             .as_deref()
     92             .unwrap_or_default()
     93             .contains("queued")
     94     );
     95 
     96     let second = (10_000..20_000)
     97         .map(|timestamp| node.prepare_automatic_mining(timestamp))
     98         .find(|plan| plan.pow_mined.is_some())
     99         .expect("automatic PoW should allow a second proof for the same tip");
    100     let second_mine = second.pow_mined.as_ref().expect("PoW should be queued");
    101     assert_ne!(second_mine.signature(), first_mine.signature());
    102     assert_eq!(node.ledger().pending().len(), first_pending + 1);
    103 
    104     for timestamp in 20_000..20_010 {
    105         assert!(node.prepare_automatic_mining(timestamp).pow_mined.is_none());
    106     }
    107     assert_eq!(node.ledger().pending().len(), first_pending + 1);
    108     assert_eq!(
    109         node.status().mining.last_auto_pow_mine_status.as_deref(),
    110         Some("waiting for next chain tip after queued mine actions")
    111     );
    112     assert!(node.ledger().pending_blinded_transactions().is_empty());
    113 }
    114 
    115 #[test]
    116 fn automatic_pow_mining_waits_after_queueing_anchor_limit_for_tip() {
    117     let wallet = Wallet::from_seed("automatic-pow-independent-wallet");
    118     let mut allocations = BTreeMap::new();
    119     allocations.insert(wallet.address().to_string(), 1);
    120     let mut node = NodeCore::new(NodeConfig {
    121         wallet,
    122         genesis_allocations: allocations,
    123         vdf_rounds: 10,
    124         burn_per_block: 0,
    125         burn_fee: 0,
    126         pow_mining_workers: 1,
    127         recovery_vdf_top_rank_percent: 100,
    128     });
    129 
    130     node.set_pow_mining_enabled(true);
    131     let first_mined = (1..10_000)
    132         .find_map(|_| node.prepare_automatic_pow_mining().unwrap())
    133         .expect("PoW should eventually queue a mine action");
    134     let anchor = match first_mined {
    135         Transaction::Mine { ref anchor, .. } => anchor.clone(),
    136         _ => panic!("expected mine action"),
    137     };
    138     assert_eq!(node.ledger().pending_mine_count_for_anchor(&anchor), 1);
    139 
    140     (1..10_000)
    141         .find_map(|_| node.prepare_automatic_pow_mining().unwrap())
    142         .expect("PoW should allow a second mine action for the same tip");
    143     assert_eq!(
    144         node.ledger().pending_mine_count_for_anchor(&anchor),
    145         MINE_ACTIONS_PER_ANCHOR_LIMIT
    146     );
    147     assert!(node.prepare_automatic_pow_mining().unwrap().is_none());
    148     assert!(node.auto_pow_mine_cursor.is_none());
    149 }
    150 
    151 #[test]
    152 fn disabling_automatic_pow_mining_clears_local_work() {
    153     let wallet = Wallet::from_seed("automatic-pow-disable-wallet");
    154     let mut allocations = BTreeMap::new();
    155     allocations.insert(wallet.address().to_string(), 1);
    156     let mut node = NodeCore::new(NodeConfig {
    157         wallet,
    158         genesis_allocations: allocations,
    159         vdf_rounds: 10,
    160         burn_per_block: 0,
    161         burn_fee: 0,
    162         pow_mining_workers: 1,
    163         recovery_vdf_top_rank_percent: 100,
    164     });
    165 
    166     node.set_pow_mining_enabled(true);
    167     assert!(node.pow_mining_enabled());
    168     node.prepare_automatic_pow_mining().unwrap();
    169     assert!(node.auto_pow_mine_cursor.is_some());
    170     assert!(node.status().mining.last_auto_pow_mine_status.is_some());
    171 
    172     node.set_pow_mining_enabled(false);
    173 
    174     assert!(!node.pow_mining_enabled());
    175     assert!(node.auto_pow_mine_cursor.is_none());
    176     assert!(node.status().mining.last_auto_pow_mine_status.is_none());
    177 }
    178 
    179 #[test]
    180 fn automatic_pow_mining_workers_are_clamped_and_reported() {
    181     let wallet = Wallet::from_seed("automatic-pow-workers-wallet");
    182     let mut node = NodeCore::new(NodeConfig {
    183         wallet,
    184         genesis_allocations: BTreeMap::new(),
    185         vdf_rounds: 10,
    186         burn_per_block: 0,
    187         burn_fee: 0,
    188         pow_mining_workers: 99,
    189         recovery_vdf_top_rank_percent: 100,
    190     });
    191 
    192     assert_eq!(node.pow_mining_workers(), MAX_POW_MINING_WORKERS);
    193     assert_eq!(
    194         node.status().mining.max_pow_mining_workers,
    195         MAX_POW_MINING_WORKERS
    196     );
    197 
    198     node.set_pow_mining_workers(0);
    199 
    200     assert_eq!(node.pow_mining_workers(), DEFAULT_POW_MINING_WORKERS);
    201     assert_eq!(
    202         node.status().mining.pow_mining_workers,
    203         DEFAULT_POW_MINING_WORKERS
    204     );
    205 }
    206 
    207 #[test]
    208 fn automatic_pow_mining_skips_unspendable_owned_blinded_payloads() {
    209     let alice = Wallet::from_seed("automatic-pow-stale-owned-blind-alice");
    210     let bob = Wallet::from_seed("automatic-pow-stale-owned-blind-bob");
    211     let mut allocations = BTreeMap::new();
    212     allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    213     allocations.insert(bob.address().to_string(), MICRO_IUNA);
    214     let ledger = Ledger::new_with_genesis_burns(
    215         allocations,
    216         vec![GenesisBurn::new(bob.address(), MICRO_IUNA)],
    217         10,
    218     )
    219     .unwrap();
    220     let mut node = NodeCore::from_ledger(alice.clone(), ledger, 0);
    221 
    222     let blinded = node
    223         .blinded_burn_with_fee(MICRO_IUNA / 10, 7, node.chain_height() + 4)
    224         .unwrap();
    225     let mut finalizer_ledger = node.ledger().clone();
    226     let leader_burn = finalizer_ledger.build_burn(&bob, 1, 0).unwrap();
    227     finalizer_ledger.submit_transaction(leader_burn).unwrap();
    228     let commit_block = finalizer_ledger.mine_next_block(&bob, 1).unwrap();
    229     assert!(
    230         commit_block
    231             .blinded_transactions
    232             .iter()
    233             .any(|tx| tx.commitment == blinded.commitment)
    234     );
    235     finalizer_ledger.apply_block(commit_block).unwrap();
    236     assert!(node.import_verified_ledger(finalizer_ledger).unwrap());
    237     assert!(
    238         node.ledger()
    239             .has_unrevealed_blinded_transaction(&blinded.commitment)
    240     );
    241 
    242     node.set_pow_mining_enabled(true);
    243 
    244     assert!(node.prepare_automatic_pow_mining().is_ok());
    245 }
    246 
    247 #[test]
    248 fn automatic_pow_mining_skips_stale_local_anchor_reservation() {
    249     let wallet = Wallet::from_seed("automatic-pow-stale-anchor-wallet");
    250     let mut stale_allocations = BTreeMap::new();
    251     stale_allocations.insert(wallet.address().to_string(), MICRO_IUNA);
    252     let stale_ledger = Ledger::new(stale_allocations, 10);
    253     let stale_anchor = stale_ledger.build_burn(&wallet, 1, 0).unwrap();
    254     let live_ledger = Ledger::new(BTreeMap::new(), 10);
    255     let mut node = NodeCore::from_ledger(wallet.clone(), live_ledger, 0);
    256     node.local_block_anchor_burn = Some((node.chain_height(), stale_anchor));
    257     node.set_pow_mining_enabled(true);
    258 
    259     assert!(node.prepare_automatic_pow_mining().is_ok());
    260 }
    261 
    262 #[test]
    263 fn automatic_finalization_does_not_tick_pow_mining() {
    264     let wallet = Wallet::from_seed("automatic-pow-separated-finalizer-wallet");
    265     let mut node = NodeCore::new(NodeConfig {
    266         wallet,
    267         genesis_allocations: BTreeMap::new(),
    268         vdf_rounds: 10,
    269         burn_per_block: 0,
    270         burn_fee: 0,
    271         pow_mining_workers: 1,
    272         recovery_vdf_top_rank_percent: 100,
    273     });
    274 
    275     node.set_pow_mining_enabled(true);
    276     let _ = node.prepare_automatic_finalization(1);
    277 
    278     assert!(node.auto_pow_mine_cursor.is_none());
    279 }
    280 
    281 #[test]
    282 fn automatic_finalization_prepares_recovery_after_ticket_timeout() {
    283     let alice = Wallet::from_seed("automatic-recovery-alice");
    284     let bob = Wallet::from_seed("automatic-recovery-bob");
    285     let mut allocations = BTreeMap::new();
    286     allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    287     allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    288     let ledger =
    289         Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10)
    290             .unwrap();
    291     let mut node = NodeCore::from_ledger(bob, ledger, 1);
    292 
    293     let early = node.prepare_automatic_finalization(RECOVERY_BLOCK_DELAY_MS - 1);
    294     assert!(early.work.is_none());
    295     assert!(
    296         early
    297             .skipped_reason
    298             .as_deref()
    299             .unwrap_or_default()
    300             .contains("waiting for selected finalizer")
    301     );
    302 
    303     let recovery = node.prepare_automatic_finalization(RECOVERY_BLOCK_DELAY_MS);
    304     let work = recovery.work.expect("recovery work should be prepared");
    305     let block = work.finish(
    306         node.wallet.unlocked().unwrap(),
    307         "preverified-vdf".to_string(),
    308     );
    309 
    310     assert_eq!(block.finalizer_mode, FinalizerMode::Recovery);
    311     assert!(block.leader_proof.is_none());
    312 }
    313 
    314 #[test]
    315 fn automatic_finalization_respects_zero_recovery_vdf_threshold() {
    316     let alice = Wallet::from_seed("automatic-recovery-zero-alice");
    317     let bob = Wallet::from_seed("automatic-recovery-zero-bob");
    318     let mut allocations = BTreeMap::new();
    319     allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    320     allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    321     let ledger =
    322         Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10)
    323             .unwrap();
    324     let mut node = NodeCore::from_ledger(bob, ledger, 1);
    325     node.set_recovery_vdf_top_rank_percent(0);
    326 
    327     let recovery = node.prepare_automatic_finalization(RECOVERY_BLOCK_DELAY_MS);
    328 
    329     assert!(recovery.work.is_none());
    330 }
    331 
    332 #[test]
    333 fn automatic_non_leader_burn_is_queued_as_blinded() {
    334     let alice = Wallet::from_seed("auto-blinded-burn-alice");
    335     let bob = Wallet::from_seed("auto-blinded-burn-bob");
    336     let carol = Wallet::from_seed("auto-blinded-burn-carol");
    337     let finalizers = [alice.clone(), bob.clone()];
    338     let mut allocations = BTreeMap::new();
    339     allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    340     allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    341     allocations.insert(carol.address().to_string(), 10 * MICRO_IUNA);
    342     let ledger = Ledger::new_with_genesis_burns(
    343         allocations,
    344         finalizers
    345             .iter()
    346             .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    347             .collect(),
    348         1,
    349     )
    350     .unwrap();
    351     assert_eq!(ledger.finalizer_rank_for_next_block(carol.address()), None);
    352     let mut node =
    353         NodeCore::from_ledger_with_burn_fee_and_enabled(carol, ledger, true, MICRO_IUNA / 10, 1);
    354 
    355     let plan = node.prepare_automatic_finalization(1);
    356     let outbox = node.drain_outbox();
    357 
    358     assert!(plan.burned.is_some());
    359     assert!(node.ledger().pending().is_empty());
    360     assert_eq!(node.ledger().pending_blinded_transactions().len(), 1);
    361     assert!(
    362         outbox
    363             .iter()
    364             .any(|envelope| matches!(envelope, GossipEnvelope::BlindedTransaction(_)))
    365     );
    366 }
    367 
    368 #[test]
    369 fn automatic_fallback_finalizer_prepares_anchor_and_blinded_burn() {
    370     let alice = Wallet::from_seed("auto-fallback-burn-alice");
    371     let bob = Wallet::from_seed("auto-fallback-burn-bob");
    372     let finalizers = [alice.clone(), bob.clone()];
    373     let mut allocations = BTreeMap::new();
    374     allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    375     allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    376     let ledger = Ledger::new_with_genesis_burns(
    377         allocations,
    378         finalizers
    379             .iter()
    380             .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    381             .collect(),
    382         1,
    383     )
    384     .unwrap();
    385     let fallback = finalizers
    386         .iter()
    387         .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1))
    388         .unwrap()
    389         .clone();
    390     let mut node =
    391         NodeCore::from_ledger_with_burn_fee_and_enabled(fallback, ledger, true, MICRO_IUNA / 10, 1);
    392 
    393     let plan = node.prepare_automatic_finalization(1);
    394     let outbox = node.drain_outbox();
    395 
    396     assert!(plan.burned.is_some());
    397     assert!(
    398         plan.skipped_reason.is_none(),
    399         "fallback should not be skipped: {:?}",
    400         plan.skipped_reason
    401     );
    402     assert!(node.ledger().pending().is_empty());
    403     assert_eq!(node.ledger().pending_blinded_transactions().len(), 1);
    404     let (_, anchor_burn) = node
    405         .local_block_anchor_burn
    406         .as_ref()
    407         .expect("fallback anchor burn should be held locally");
    408     let anchor_signature = anchor_burn.signature().to_string();
    409     assert_eq!(anchor_burn.amount(), super::AUTO_BLOCK_ANCHOR_BURN_AMOUNT);
    410     assert!(
    411         outbox
    412             .iter()
    413             .any(|envelope| matches!(envelope, GossipEnvelope::BlindedTransaction(_)))
    414     );
    415     let work = plan.work.expect("fallback work should be prepared");
    416     let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds());
    417     let block = node
    418         .complete_prepared_block_at(work, vdf_output, VDF_TARGET_BLOCK_MS * 2)
    419         .unwrap();
    420 
    421     assert_eq!(block.finalizer_rank, 1);
    422     assert_eq!(block.finalizer_mode, FinalizerMode::Ticket);
    423     assert!(block.transactions.iter().any(|transaction| {
    424         transaction.is_burn() && transaction.amount() == super::AUTO_BLOCK_ANCHOR_BURN_AMOUNT
    425     }));
    426     assert_eq!(
    427         block
    428             .transactions
    429             .first()
    430             .map(|transaction| transaction.signature()),
    431         Some(anchor_signature.as_str())
    432     );
    433     assert!(!block.blinded_transactions.is_empty());
    434     assert_eq!(node.ledger().height(), 1);
    435 }
    436 
    437 #[test]
    438 fn automatic_leader_prepares_anchor_and_blinded_burn() {
    439     let alice = Wallet::from_seed("auto-plaintext-burn-alice");
    440     let bob = Wallet::from_seed("auto-plaintext-burn-bob");
    441     let finalizers = [alice.clone(), bob.clone()];
    442     let mut allocations = BTreeMap::new();
    443     allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    444     allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    445     let mut ledger = Ledger::new_with_genesis_burns(
    446         allocations,
    447         finalizers
    448             .iter()
    449             .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    450             .collect(),
    451         1,
    452     )
    453     .unwrap();
    454     let leader = ledger.expected_leader_for_next_block().unwrap();
    455     let leader_wallet = finalizers
    456         .iter()
    457         .find(|wallet| wallet.address() == leader)
    458         .unwrap()
    459         .clone();
    460     for wallet in &finalizers {
    461         let split = ledger
    462             .build_transfer(wallet, wallet.address(), MICRO_IUNA, 0)
    463             .unwrap();
    464         ledger.submit_transaction(split).unwrap();
    465     }
    466     let anchor = ledger.build_burn(&leader_wallet, 1, 0).unwrap();
    467     ledger.submit_transaction(anchor).unwrap();
    468     let split_block = ledger.mine_next_block(&leader_wallet, 1).unwrap();
    469     ledger.apply_block(split_block).unwrap();
    470     let leader = ledger.expected_leader_for_next_block().unwrap();
    471     let leader_wallet = finalizers
    472         .iter()
    473         .find(|wallet| wallet.address() == leader)
    474         .unwrap()
    475         .clone();
    476     let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(
    477         leader_wallet,
    478         ledger,
    479         true,
    480         MICRO_IUNA / 10,
    481         1,
    482     );
    483 
    484     let plan = node.prepare_automatic_finalization(1);
    485     let outbox = node.drain_outbox();
    486 
    487     assert!(plan.burned.is_some());
    488     assert!(node.ledger().pending().is_empty());
    489     assert_eq!(node.ledger().pending_blinded_transactions().len(), 1);
    490     let (_, anchor_burn) = node
    491         .local_block_anchor_burn
    492         .as_ref()
    493         .expect("leader anchor burn should be held locally");
    494     assert_eq!(anchor_burn.amount(), super::AUTO_BLOCK_ANCHOR_BURN_AMOUNT);
    495     assert!(
    496         outbox
    497             .iter()
    498             .any(|envelope| matches!(envelope, GossipEnvelope::BlindedTransaction(_)))
    499     );
    500     assert!(node.prepare_automatic_finalization(1).work.is_some());
    501 }
    502 
    503 #[test]
    504 fn automatic_pow_mining_uses_protocol_finalizer_fee() {
    505     let wallet = Wallet::from_seed("automatic-pow-mining-fee-wallet");
    506     let ledger = Ledger::new_with_genesis_burns(
    507         BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]),
    508         vec![GenesisBurn::new(wallet.address(), 1)],
    509         10,
    510     )
    511     .unwrap();
    512     let mut node = NodeCore::from_ledger(wallet, ledger, 0);
    513 
    514     node.set_pow_mining_enabled(true);
    515     let plan = (1..10_000)
    516         .map(|timestamp| node.prepare_automatic_mining(timestamp))
    517         .find(|plan| plan.pow_mined.is_some())
    518         .expect("bounded PoW search should eventually find a proof");
    519     let mine = plan.pow_mined.expect("PoW should be queued");
    520 
    521     assert_eq!(mine.fee(), MINE_FINALIZER_FEE);
    522     assert_eq!(mine.amount(), crate::domain::MINE_REWARD);
    523     assert_eq!(
    524         node.status().mining.automatic_pow_mine_fee,
    525         MINE_FINALIZER_FEE
    526     );
    527 }