tests.rs (20055B)
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 selected_finalizer_runs_vdf_with_zero_recovery_vdf_threshold() { 334 let alice = Wallet::from_seed("automatic-selected-zero-alice"); 335 let mut allocations = BTreeMap::new(); 336 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 337 let ledger = 338 Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10) 339 .unwrap(); 340 assert_eq!( 341 ledger.expected_leader_for_next_block().as_deref(), 342 Some(alice.address()) 343 ); 344 let mut node = NodeCore::from_ledger(alice.clone(), ledger, 1); 345 node.set_recovery_vdf_top_rank_percent(0); 346 347 let plan = node.prepare_automatic_finalization(1); 348 349 assert!(plan.work.is_some()); 350 assert!(plan.skipped_reason.is_none()); 351 } 352 353 #[test] 354 fn automatic_non_leader_burn_is_queued_as_blinded() { 355 let alice = Wallet::from_seed("auto-blinded-burn-alice"); 356 let bob = Wallet::from_seed("auto-blinded-burn-bob"); 357 let carol = Wallet::from_seed("auto-blinded-burn-carol"); 358 let finalizers = [alice.clone(), bob.clone()]; 359 let mut allocations = BTreeMap::new(); 360 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 361 allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); 362 allocations.insert(carol.address().to_string(), 10 * MICRO_IUNA); 363 let ledger = Ledger::new_with_genesis_burns( 364 allocations, 365 finalizers 366 .iter() 367 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) 368 .collect(), 369 1, 370 ) 371 .unwrap(); 372 assert_eq!(ledger.finalizer_rank_for_next_block(carol.address()), None); 373 let mut node = 374 NodeCore::from_ledger_with_burn_fee_and_enabled(carol, ledger, true, MICRO_IUNA / 10, 1); 375 376 let plan = node.prepare_automatic_finalization(1); 377 let outbox = node.drain_outbox(); 378 379 assert!(plan.burned.is_some()); 380 assert!(node.ledger().pending().is_empty()); 381 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 382 assert!( 383 outbox 384 .iter() 385 .any(|envelope| matches!(envelope, GossipEnvelope::BlindedTransaction(_))) 386 ); 387 } 388 389 #[test] 390 fn automatic_fallback_finalizer_prepares_anchor_and_blinded_burn() { 391 let alice = Wallet::from_seed("auto-fallback-burn-alice"); 392 let bob = Wallet::from_seed("auto-fallback-burn-bob"); 393 let finalizers = [alice.clone(), bob.clone()]; 394 let mut allocations = BTreeMap::new(); 395 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 396 allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); 397 let ledger = Ledger::new_with_genesis_burns( 398 allocations, 399 finalizers 400 .iter() 401 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) 402 .collect(), 403 1, 404 ) 405 .unwrap(); 406 let fallback = finalizers 407 .iter() 408 .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1)) 409 .unwrap() 410 .clone(); 411 let mut node = 412 NodeCore::from_ledger_with_burn_fee_and_enabled(fallback, ledger, true, MICRO_IUNA / 10, 1); 413 414 let plan = node.prepare_automatic_finalization(1); 415 let outbox = node.drain_outbox(); 416 417 assert!(plan.burned.is_some()); 418 assert!( 419 plan.skipped_reason.is_none(), 420 "fallback should not be skipped: {:?}", 421 plan.skipped_reason 422 ); 423 assert!(node.ledger().pending().is_empty()); 424 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 425 let (_, anchor_burn) = node 426 .local_block_anchor_burn 427 .as_ref() 428 .expect("fallback anchor burn should be held locally"); 429 let anchor_signature = anchor_burn.signature().to_string(); 430 assert_eq!(anchor_burn.amount(), super::AUTO_BLOCK_ANCHOR_BURN_AMOUNT); 431 assert!( 432 outbox 433 .iter() 434 .any(|envelope| matches!(envelope, GossipEnvelope::BlindedTransaction(_))) 435 ); 436 let work = plan.work.expect("fallback work should be prepared"); 437 let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds()); 438 let block = node 439 .complete_prepared_block_at(work, vdf_output, VDF_TARGET_BLOCK_MS * 2) 440 .unwrap(); 441 442 assert_eq!(block.finalizer_rank, 1); 443 assert_eq!(block.finalizer_mode, FinalizerMode::Ticket); 444 assert!(block.transactions.iter().any(|transaction| { 445 transaction.is_burn() && transaction.amount() == super::AUTO_BLOCK_ANCHOR_BURN_AMOUNT 446 })); 447 assert_eq!( 448 block 449 .transactions 450 .first() 451 .map(|transaction| transaction.signature()), 452 Some(anchor_signature.as_str()) 453 ); 454 assert!(!block.blinded_transactions.is_empty()); 455 assert_eq!(node.ledger().height(), 1); 456 } 457 458 #[test] 459 fn automatic_leader_prepares_anchor_and_blinded_burn() { 460 let alice = Wallet::from_seed("auto-plaintext-burn-alice"); 461 let bob = Wallet::from_seed("auto-plaintext-burn-bob"); 462 let finalizers = [alice.clone(), bob.clone()]; 463 let mut allocations = BTreeMap::new(); 464 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 465 allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); 466 let mut ledger = Ledger::new_with_genesis_burns( 467 allocations, 468 finalizers 469 .iter() 470 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) 471 .collect(), 472 1, 473 ) 474 .unwrap(); 475 let leader = ledger.expected_leader_for_next_block().unwrap(); 476 let leader_wallet = finalizers 477 .iter() 478 .find(|wallet| wallet.address() == leader) 479 .unwrap() 480 .clone(); 481 for wallet in &finalizers { 482 let split = ledger 483 .build_transfer(wallet, wallet.address(), MICRO_IUNA, 0) 484 .unwrap(); 485 ledger.submit_transaction(split).unwrap(); 486 } 487 let anchor = ledger.build_burn(&leader_wallet, 1, 0).unwrap(); 488 ledger.submit_transaction(anchor).unwrap(); 489 let split_block = ledger.mine_next_block(&leader_wallet, 1).unwrap(); 490 ledger.apply_block(split_block).unwrap(); 491 let leader = ledger.expected_leader_for_next_block().unwrap(); 492 let leader_wallet = finalizers 493 .iter() 494 .find(|wallet| wallet.address() == leader) 495 .unwrap() 496 .clone(); 497 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( 498 leader_wallet, 499 ledger, 500 true, 501 MICRO_IUNA / 10, 502 1, 503 ); 504 505 let plan = node.prepare_automatic_finalization(1); 506 let outbox = node.drain_outbox(); 507 508 assert!(plan.burned.is_some()); 509 assert!(node.ledger().pending().is_empty()); 510 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 511 let (_, anchor_burn) = node 512 .local_block_anchor_burn 513 .as_ref() 514 .expect("leader anchor burn should be held locally"); 515 assert_eq!(anchor_burn.amount(), super::AUTO_BLOCK_ANCHOR_BURN_AMOUNT); 516 assert!( 517 outbox 518 .iter() 519 .any(|envelope| matches!(envelope, GossipEnvelope::BlindedTransaction(_))) 520 ); 521 assert!(node.prepare_automatic_finalization(1).work.is_some()); 522 } 523 524 #[test] 525 fn automatic_pow_mining_uses_protocol_finalizer_fee() { 526 let wallet = Wallet::from_seed("automatic-pow-mining-fee-wallet"); 527 let ledger = Ledger::new_with_genesis_burns( 528 BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]), 529 vec![GenesisBurn::new(wallet.address(), 1)], 530 10, 531 ) 532 .unwrap(); 533 let mut node = NodeCore::from_ledger(wallet, ledger, 0); 534 535 node.set_pow_mining_enabled(true); 536 let plan = (1..10_000) 537 .map(|timestamp| node.prepare_automatic_mining(timestamp)) 538 .find(|plan| plan.pow_mined.is_some()) 539 .expect("bounded PoW search should eventually find a proof"); 540 let mine = plan.pow_mined.expect("PoW should be queued"); 541 542 assert_eq!(mine.fee(), MINE_FINALIZER_FEE); 543 assert_eq!(mine.amount(), crate::domain::MINE_REWARD); 544 assert_eq!( 545 node.status().mining.automatic_pow_mine_fee, 546 MINE_FINALIZER_FEE 547 ); 548 }