iuna.rs (91937B)
1 use std::{ 2 collections::BTreeMap, 3 time::{SystemTime, UNIX_EPOCH}, 4 }; 5 6 use iuna::{ 7 adapters::chain_store::SqliteChainStore, 8 app::{ 9 DEFAULT_BURN_PER_BLOCK, GossipEnvelope, InMemoryNetwork, NodeConfig, NodeCore, PeerBook, 10 PeerDirection, TRANSACTION_BATCH_LIMIT, 11 }, 12 domain::{ 13 Amount, BLOCK_REWARD, DEFAULT_FEE_PER_BYTE, FinalizerMode, GenesisBurn, Ledger, 14 MAX_BLOCK_BYTES, MICRO_IUNA, MINE_REWARD, RECOVERY_BLOCK_DELAY_MS, 15 TransactionSubmitOutcome, VDF_TARGET_BLOCK_MS, Wallet, revealed_blinded_transactions, 16 run_vdf, verify_vdf, 17 }, 18 }; 19 use tempfile::tempdir; 20 21 fn iuna(amount: Amount) -> Amount { 22 amount * MICRO_IUNA 23 } 24 25 fn node(_network_key: &str, wallet: Wallet, allocations: BTreeMap<String, Amount>) -> NodeCore { 26 NodeCore::new(NodeConfig { 27 wallet, 28 genesis_allocations: allocations, 29 vdf_rounds: 25, 30 burn_per_block: DEFAULT_BURN_PER_BLOCK, 31 burn_fee: 1, 32 pow_mining_workers: 1, 33 recovery_vdf_top_rank_percent: 100, 34 }) 35 } 36 37 fn wallets(names: &[&str]) -> Vec<Wallet> { 38 names.iter().map(|name| Wallet::from_seed(name)).collect() 39 } 40 41 fn allocations(wallets: &[Wallet], amount: Amount) -> BTreeMap<String, Amount> { 42 wallets 43 .iter() 44 .map(|wallet| (wallet.address().to_string(), amount)) 45 .collect() 46 } 47 48 fn unix_now_ms() -> u64 { 49 SystemTime::now() 50 .duration_since(UNIX_EPOCH) 51 .unwrap_or_default() 52 .as_millis() 53 .try_into() 54 .unwrap_or(u64::MAX) 55 } 56 57 fn next_ticket_slot_timestamp(ledger: &Ledger, offset_ms: u64) -> u64 { 58 ledger 59 .chain() 60 .last() 61 .expect("ledger has genesis") 62 .timestamp_ms 63 .saturating_add(VDF_TARGET_BLOCK_MS) 64 .saturating_add(offset_ms) 65 } 66 67 fn mine_wallet_burn_block(ledger: &mut Ledger, wallet: &Wallet, timestamp_ms: u64) -> String { 68 let burn = ledger.build_burn(wallet, 1, 0).unwrap(); 69 ledger.submit_transaction(burn).unwrap(); 70 let block = ledger.mine_next_block(wallet, timestamp_ms).unwrap(); 71 let hash = block.hash.clone(); 72 ledger.apply_block(block).unwrap(); 73 hash 74 } 75 76 fn submit_burn(ledger: &mut Ledger, wallet: &Wallet, amount: Amount) { 77 let tx = ledger.build_burn(wallet, amount, 0).unwrap(); 78 ledger.submit_transaction(tx).unwrap(); 79 } 80 81 fn queue_plaintext_burn( 82 node: &mut NodeCore, 83 wallet: &Wallet, 84 amount: Amount, 85 ) -> iuna::domain::Transaction { 86 let tx = node.ledger().build_burn(wallet, amount, 0).unwrap(); 87 node.receive_transaction(tx.clone()).unwrap(); 88 tx 89 } 90 91 fn queue_plaintext_transfer( 92 node: &mut NodeCore, 93 wallet: &Wallet, 94 to: impl Into<String>, 95 amount: Amount, 96 ) -> iuna::domain::Transaction { 97 let tx = node.ledger().build_transfer(wallet, to, amount, 0).unwrap(); 98 node.receive_transaction(tx.clone()).unwrap(); 99 tx 100 } 101 102 fn burn_tx(ledger: &Ledger, wallet: &Wallet, amount: Amount) -> iuna::domain::Transaction { 103 ledger.build_burn(wallet, amount, 0).unwrap() 104 } 105 106 fn transfer_tx( 107 ledger: &Ledger, 108 wallet: &Wallet, 109 to: impl Into<String>, 110 amount: Amount, 111 ) -> iuna::domain::Transaction { 112 ledger.build_transfer(wallet, to, amount, 0).unwrap() 113 } 114 115 fn transfer_fee_tx( 116 ledger: &Ledger, 117 wallet: &Wallet, 118 to: impl Into<String>, 119 amount: Amount, 120 fee: Amount, 121 ) -> iuna::domain::Transaction { 122 ledger.build_transfer(wallet, to, amount, fee).unwrap() 123 } 124 125 fn fork_with_better_vrf_block( 126 base: &Ledger, 127 wallet: &Wallet, 128 local_fork_block_hash: &str, 129 first_timestamp_ms: u64, 130 ) -> Option<Ledger> { 131 for offset in 0..10_000 { 132 let mut candidate = base.clone(); 133 let timestamp_ms = next_ticket_slot_timestamp(&candidate, first_timestamp_ms + offset); 134 let hash = mine_wallet_burn_block(&mut candidate, wallet, timestamp_ms); 135 if hash.as_str() < local_fork_block_hash { 136 return Some(candidate); 137 } 138 } 139 None 140 } 141 142 fn fork_with_worse_vrf_block( 143 base: &Ledger, 144 wallet: &Wallet, 145 local_fork_block_hash: &str, 146 first_timestamp_ms: u64, 147 ) -> Option<Ledger> { 148 for offset in 0..10_000 { 149 let mut candidate = base.clone(); 150 let timestamp_ms = next_ticket_slot_timestamp(&candidate, first_timestamp_ms + offset); 151 let hash = mine_wallet_burn_block(&mut candidate, wallet, timestamp_ms); 152 if hash.as_str() > local_fork_block_hash { 153 return Some(candidate); 154 } 155 } 156 None 157 } 158 159 fn starter_node(wallet: Wallet) -> NodeCore { 160 let mut genesis = BTreeMap::new(); 161 genesis.insert(wallet.address().to_string(), 1); 162 let ledger = 163 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 25) 164 .unwrap(); 165 NodeCore::from_ledger_with_burn_fee_and_enabled( 166 wallet, 167 ledger, 168 true, 169 DEFAULT_BURN_PER_BLOCK, 170 DEFAULT_FEE_PER_BYTE, 171 ) 172 } 173 174 #[test] 175 fn genesis_burn_starts_chain_with_reward_and_first_leader() { 176 let alice = Wallet::from_seed("alice"); 177 let node = starter_node(alice.clone()); 178 179 let genesis = &node.ledger().chain()[0]; 180 assert_eq!(node.ledger().balance_of(alice.address()), BLOCK_REWARD); 181 assert_eq!(genesis.height, 0); 182 assert_eq!(genesis.miner, alice.address()); 183 assert_eq!(genesis.reward, BLOCK_REWARD); 184 assert_eq!(genesis.transactions.len(), 1); 185 assert!(genesis.transactions[0].is_burn()); 186 assert_eq!(genesis.transactions[0].amount(), 1); 187 assert_eq!( 188 node.ledger().expected_leader_for_next_block().as_deref(), 189 Some(alice.address()) 190 ); 191 } 192 193 #[test] 194 fn burn_amount_weights_leader_selection() { 195 let mut high_weight_leaders = 0; 196 for sample in 0..100 { 197 let low = Wallet::from_seed(&format!("weighted-low-{sample}")); 198 let high = Wallet::from_seed(&format!("weighted-high-{sample}")); 199 let mut allocations = BTreeMap::new(); 200 allocations.insert(low.address().to_string(), 1_000); 201 allocations.insert(high.address().to_string(), 1_000); 202 let ledger = Ledger::new_with_genesis_burns( 203 allocations, 204 vec![ 205 GenesisBurn::new(low.address(), 1), 206 GenesisBurn::new(high.address(), 99), 207 ], 208 25, 209 ) 210 .unwrap(); 211 212 if ledger.expected_leader_for_next_block().as_deref() == Some(high.address()) { 213 high_weight_leaders += 1; 214 } 215 } 216 217 assert!( 218 high_weight_leaders >= 90, 219 "high burn ticket should win most weighted draws, won {high_weight_leaders}/100" 220 ); 221 } 222 223 #[test] 224 fn starter_node_waits_for_a_burn_before_vdf_work() { 225 let alice = Wallet::from_seed("alice"); 226 let mut node = starter_node(alice.clone()); 227 228 let outcome = node.automatic_mine_once(1); 229 assert_eq!(outcome.burned.as_ref().map(|tx| tx.amount()), Some(1)); 230 assert!(outcome.block.is_some(), "{outcome:?}"); 231 assert_eq!(node.ledger().status().height, 1); 232 assert!(node.ledger().balance_of(alice.address()) < BLOCK_REWARD); 233 } 234 235 #[test] 236 fn burn_in_block_creates_ticket_after_maturity_delay() { 237 let alice = Wallet::from_seed("alice"); 238 let bob = Wallet::from_seed("bob"); 239 let mut allocations = BTreeMap::new(); 240 allocations.insert(alice.address().to_string(), MICRO_IUNA); 241 allocations.insert(bob.address().to_string(), MICRO_IUNA); 242 243 let mut ledger = Ledger::new(allocations, 10); 244 submit_burn(&mut ledger, &bob, 80); 245 246 let launch_leader = 247 if ledger.expected_leader_for_next_block().as_deref() == Some(alice.address()) { 248 &alice 249 } else { 250 &bob 251 }; 252 let first = ledger.mine_next_block(launch_leader, 1).unwrap(); 253 ledger.apply_block(first).unwrap(); 254 255 for height in 2..=3 { 256 let leader_wallet = 257 if ledger.expected_leader_for_next_block().as_deref() == Some(alice.address()) { 258 &alice 259 } else { 260 &bob 261 }; 262 submit_burn(&mut ledger, leader_wallet, 1); 263 let block = ledger.mine_next_block(leader_wallet, height).unwrap(); 264 ledger.apply_block(block).unwrap(); 265 } 266 267 assert_eq!( 268 ledger.expected_leader_for_next_block().as_deref(), 269 Some(bob.address()) 270 ); 271 272 assert!( 273 ledger.mine_next_block(&alice, 4).is_err(), 274 "the block 1 burn should not be eligible before its maturity delay, and only Bob should hold it at block 4" 275 ); 276 277 submit_burn(&mut ledger, &bob, 1); 278 assert!(ledger.mine_next_block(&bob, 4).is_ok()); 279 } 280 281 #[test] 282 fn transfer_and_burn_update_balances_when_block_is_applied() { 283 let alice = Wallet::from_seed("alice"); 284 let bob = Wallet::from_seed("bob"); 285 let mut allocations = BTreeMap::new(); 286 allocations.insert(alice.address().to_string(), iuna(1_000)); 287 allocations.insert(bob.address().to_string(), iuna(100)); 288 289 let mut ledger = Ledger::new(allocations, 10); 290 let transfer = transfer_tx(&ledger, &alice, bob.address(), iuna(125)); 291 ledger.submit_transaction(transfer).unwrap(); 292 let burn = burn_tx(&ledger, &alice, iuna(25)); 293 ledger.submit_transaction(burn).unwrap(); 294 let block = ledger.mine_next_block(&alice, 1).unwrap(); 295 ledger.apply_block(block).unwrap(); 296 297 assert_eq!(ledger.balance_of(alice.address()), iuna(850)); 298 assert_eq!(ledger.balance_of(bob.address()), iuna(225)); 299 } 300 301 #[test] 302 fn forged_transaction_is_rejected() { 303 let alice = Wallet::from_seed("alice"); 304 let bob = Wallet::from_seed("bob"); 305 let mut allocations = BTreeMap::new(); 306 allocations.insert(alice.address().to_string(), MICRO_IUNA); 307 allocations.insert(bob.address().to_string(), MICRO_IUNA); 308 let mut ledger = Ledger::new(allocations, 10); 309 310 let mut forged = burn_tx(&ledger, &bob, 10); 311 if let iuna::domain::Transaction::Burn { inputs, .. } = &mut forged { 312 inputs[0].owner = alice.address().to_string(); 313 } 314 315 let error = ledger.submit_transaction(forged).unwrap_err(); 316 assert!(error.to_string().contains("signature")); 317 } 318 319 #[test] 320 fn block_with_forged_transaction_is_rejected() { 321 let alice = Wallet::from_seed("alice"); 322 let mut allocations = BTreeMap::new(); 323 allocations.insert(alice.address().to_string(), MICRO_IUNA); 324 let mut ledger = Ledger::new(allocations, 10); 325 submit_burn(&mut ledger, &alice, 1); 326 327 let mut block = ledger.mine_next_block(&alice, 1).unwrap(); 328 if let iuna::domain::Transaction::Burn { signature, .. } = &mut block.transactions[0] { 329 signature.push_str("00"); 330 } 331 block.vdf_output = run_vdf(&block.vdf_seed(), block.vdf_rounds); 332 block.hash = block.compute_hash(); 333 334 let error = ledger.apply_block(block).unwrap_err(); 335 assert!(error.to_string().contains("signature")); 336 } 337 338 #[test] 339 fn mine_action_uses_fixed_reward_and_finalizer_fee() { 340 let alice = Wallet::from_seed("alice"); 341 let mut allocations = BTreeMap::new(); 342 allocations.insert(alice.address().to_string(), 2 * MICRO_IUNA); 343 344 let mut ledger = Ledger::new(allocations, 10); 345 submit_burn(&mut ledger, &alice, MICRO_IUNA); 346 let mine = ledger.build_mine(alice.address()).unwrap(); 347 assert_eq!(mine.amount(), MINE_REWARD); 348 assert_eq!(mine.fee(), MICRO_IUNA); 349 ledger.submit_transaction(mine.clone()).unwrap(); 350 let block = ledger.mine_next_block(&alice, 1).unwrap(); 351 assert_eq!(block.reward, MICRO_IUNA); 352 353 ledger.apply_block(block).unwrap(); 354 assert_eq!( 355 ledger.balance_of(alice.address()), 356 2 * MICRO_IUNA + MINE_REWARD 357 ); 358 assert!(!ledger.submit_transaction(mine).unwrap()); 359 assert_eq!( 360 ledger.balance_of(alice.address()), 361 2 * MICRO_IUNA + MINE_REWARD 362 ); 363 } 364 365 #[test] 366 fn mine_action_protocol_fee_is_paid_to_block_finalizer() { 367 let alice = Wallet::from_seed("mine-fee-alice"); 368 let bob = Wallet::from_seed("mine-fee-bob"); 369 let mut allocations = BTreeMap::new(); 370 allocations.insert(bob.address().to_string(), 2 * MICRO_IUNA); 371 372 let mut ledger = Ledger::new(allocations, 10); 373 let mine = ledger.build_mine(alice.address()).unwrap(); 374 assert_eq!(mine.amount(), MINE_REWARD); 375 assert_eq!(mine.fee(), MICRO_IUNA); 376 ledger.submit_transaction(mine).unwrap(); 377 submit_burn(&mut ledger, &bob, MICRO_IUNA); 378 379 let block = ledger.mine_next_block(&bob, 1).unwrap(); 380 assert_eq!(block.reward, MICRO_IUNA); 381 ledger.apply_block(block).unwrap(); 382 383 assert_eq!(ledger.balance_of(alice.address()), MINE_REWARD); 384 assert_eq!(ledger.balance_of(bob.address()), 2 * MICRO_IUNA); 385 } 386 387 #[test] 388 fn forged_mine_action_cannot_introduce_iuna() { 389 let alice = Wallet::from_seed("forged-mine-alice"); 390 let mut allocations = BTreeMap::new(); 391 allocations.insert(alice.address().to_string(), MICRO_IUNA); 392 let mut ledger = Ledger::new(allocations, 10); 393 394 let mut forged = ledger.build_mine(alice.address()).unwrap(); 395 if let iuna::domain::Transaction::Mine { nonce, .. } = &mut forged { 396 *nonce += 1; 397 } 398 399 let error = ledger.submit_transaction(forged).unwrap_err(); 400 assert!(format!("{error:#}").contains("mine transaction proof hash is invalid")); 401 assert_eq!(ledger.balance_of(alice.address()), MICRO_IUNA); 402 } 403 404 #[test] 405 fn burn_larger_than_mine_reward_is_paid_from_existing_utxos() { 406 let alice = Wallet::from_seed("large-burn-existing-utxos-alice"); 407 let mut allocations = BTreeMap::new(); 408 allocations.insert(alice.address().to_string(), BLOCK_REWARD + iuna(50)); 409 410 let mut ledger = Ledger::new(allocations, 10); 411 let burn_amount = BLOCK_REWARD + iuna(25); 412 let burn = ledger.build_burn(&alice, burn_amount, 0).unwrap(); 413 ledger.submit_transaction(burn).unwrap(); 414 415 let block = ledger.mine_next_block(&alice, 1).unwrap(); 416 assert_eq!(block.transactions[0].amount(), burn_amount); 417 assert_eq!(block.reward, 0); 418 419 ledger.apply_block(block).unwrap(); 420 assert_eq!(ledger.balance_of(alice.address()), iuna(25)); 421 } 422 423 #[test] 424 fn burn_larger_than_existing_balance_cannot_use_next_block_reward() { 425 let alice = Wallet::from_seed("large-burn-cannot-use-next-reward-alice"); 426 let mut allocations = BTreeMap::new(); 427 allocations.insert(alice.address().to_string(), BLOCK_REWARD); 428 429 let ledger = Ledger::new(allocations, 10); 430 let error = ledger 431 .build_burn(&alice, BLOCK_REWARD + MICRO_IUNA, 0) 432 .unwrap_err(); 433 434 assert!(format!("{error:#}").contains("insufficient funds")); 435 } 436 437 #[test] 438 fn transaction_fees_are_paid_to_the_block_finalizer() { 439 let alice = Wallet::from_seed("fee-miner-alice"); 440 let bob = Wallet::from_seed("fee-payer-bob"); 441 let mut allocations = BTreeMap::new(); 442 allocations.insert(alice.address().to_string(), MICRO_IUNA); 443 allocations.insert(bob.address().to_string(), iuna(200)); 444 445 let mut ledger = Ledger::new_with_genesis_burns( 446 allocations, 447 vec![GenesisBurn::new(alice.address(), MICRO_IUNA)], 448 10, 449 ) 450 .unwrap(); 451 submit_burn(&mut ledger, &alice, MICRO_IUNA); 452 let tx = transfer_fee_tx(&ledger, &bob, alice.address(), iuna(10), iuna(7)); 453 ledger.submit_transaction(tx).unwrap(); 454 455 let block = ledger.mine_next_block(&alice, 1).unwrap(); 456 assert_eq!(block.reward, iuna(7)); 457 ledger.apply_block(block).unwrap(); 458 459 assert_eq!(ledger.balance_of(alice.address()), BLOCK_REWARD + iuna(16)); 460 assert_eq!(ledger.balance_of(bob.address()), iuna(183)); 461 } 462 463 #[test] 464 fn miner_orders_block_transactions_by_fee_rate_after_required_burn() { 465 let wallets = wallets(&["fee-order-alice", "fee-order-bob", "fee-order-carol"]); 466 let alice = &wallets[0]; 467 let bob = &wallets[1]; 468 let carol = &wallets[2]; 469 let mut allocations = allocations(&wallets, 1_000); 470 allocations.insert(alice.address().to_string(), 1); 471 let mut ledger = 472 Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10) 473 .unwrap(); 474 let required_burn = burn_tx(&ledger, alice, 1); 475 let low_fee = transfer_fee_tx(&ledger, bob, alice.address(), 1, 1); 476 let high_fee = transfer_fee_tx(&ledger, carol, alice.address(), 1, 20); 477 ledger.submit_transaction(low_fee.clone()).unwrap(); 478 ledger.submit_transaction(high_fee.clone()).unwrap(); 479 ledger.submit_transaction(required_burn.clone()).unwrap(); 480 481 let block = ledger.mine_next_block(alice, 1).unwrap(); 482 let signatures = block 483 .transactions 484 .iter() 485 .map(|tx| tx.signature().to_string()) 486 .collect::<Vec<_>>(); 487 488 assert_eq!(signatures[0], required_burn.signature()); 489 assert!( 490 signatures 491 .iter() 492 .position(|signature| signature == high_fee.signature()) 493 < signatures 494 .iter() 495 .position(|signature| signature == low_fee.signature()) 496 ); 497 } 498 499 #[test] 500 fn oversized_blocks_are_rejected() { 501 let alice = Wallet::from_seed("oversized-block-alice"); 502 let bob = Wallet::from_seed("oversized-block-bob"); 503 let mut allocations = BTreeMap::new(); 504 allocations.insert(alice.address().to_string(), 1); 505 allocations.insert(bob.address().to_string(), 1_000); 506 let mut ledger = 507 Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10) 508 .unwrap(); 509 submit_burn(&mut ledger, &alice, 1); 510 let mut block = ledger.mine_next_block(&alice, 1).unwrap(); 511 let mut oversized_transfer = transfer_fee_tx(&ledger, &bob, alice.address(), 1, 3); 512 if let iuna::domain::Transaction::Transfer { outputs, .. } = &mut oversized_transfer { 513 outputs[0].address = "x".repeat(MAX_BLOCK_BYTES); 514 } 515 block.transactions.push(oversized_transfer); 516 block.reward = 3; 517 block.hash = block.compute_hash(); 518 519 let error = ledger.apply_block(block).unwrap_err(); 520 521 assert!(format!("{error:#}").contains("max block size")); 522 } 523 524 #[test] 525 fn transfer_that_would_overflow_recipient_balance_is_rejected() { 526 let alice = Wallet::from_seed("alice"); 527 let bob = Wallet::from_seed("bob"); 528 let mut allocations = BTreeMap::new(); 529 allocations.insert(alice.address().to_string(), 1); 530 allocations.insert(bob.address().to_string(), Amount::MAX); 531 532 let ledger = Ledger::new(allocations, 10); 533 let error = ledger 534 .build_transfer(&alice, bob.address(), 1, 0) 535 .unwrap_err(); 536 537 assert!(format!("{error:#}").contains("balance overflow")); 538 } 539 540 #[test] 541 fn mine_reward_that_would_overflow_recipient_balance_is_rejected() { 542 let alice = Wallet::from_seed("alice"); 543 let mut allocations = BTreeMap::new(); 544 allocations.insert(alice.address().to_string(), Amount::MAX); 545 546 let ledger = Ledger::new(allocations, 10); 547 let error = ledger.build_mine(alice.address()).unwrap_err(); 548 549 assert!(format!("{error:#}").contains("balance overflow")); 550 } 551 552 #[test] 553 fn block_without_mature_ticket_cannot_be_mined() { 554 let alice = Wallet::from_seed("alice"); 555 let allocations = BTreeMap::new(); 556 557 let ledger = Ledger::new(allocations, 10); 558 let error = ledger.mine_next_block(&alice, 1).unwrap_err(); 559 assert!(error.to_string().contains("mature burn ticket")); 560 } 561 562 #[test] 563 fn vdf_work_requires_at_least_one_pending_burn() { 564 let alice = Wallet::from_seed("alice"); 565 let mut allocations = BTreeMap::new(); 566 allocations.insert(alice.address().to_string(), MICRO_IUNA); 567 568 let ledger = Ledger::new(allocations, 10); 569 let error = ledger.prepare_next_block(alice.address(), 1).unwrap_err(); 570 571 assert!(format!("{error:#}").contains("at least one burn")); 572 } 573 574 #[test] 575 fn leader_block_without_burn_is_rejected() { 576 let alice = Wallet::from_seed("alice"); 577 let mut allocations = BTreeMap::new(); 578 allocations.insert(alice.address().to_string(), 1_000); 579 580 let mut ledger = Ledger::new(allocations, 10); 581 submit_burn(&mut ledger, &alice, 1); 582 let mut block = ledger.mine_next_block(&alice, 1).unwrap(); 583 block.transactions.clear(); 584 block.hash = block.compute_hash(); 585 586 let error = ledger.apply_block(block).unwrap_err(); 587 assert!(format!("{error:#}").contains("at least one burn")); 588 } 589 590 #[test] 591 fn block_hash_is_bound_to_block_contents() { 592 let alice = Wallet::from_seed("alice"); 593 let mut allocations = BTreeMap::new(); 594 allocations.insert(alice.address().to_string(), 1_000); 595 596 let mut ledger = Ledger::new(allocations, 10); 597 submit_burn(&mut ledger, &alice, 1); 598 let mut block = ledger.mine_next_block(&alice, 1).unwrap(); 599 block.timestamp_ms += 1; 600 601 let error = ledger.apply_block(block).unwrap_err(); 602 assert!(error.to_string().contains("block hash is invalid")); 603 } 604 605 #[test] 606 fn automatic_mining_burns_configured_amount_once_per_height() { 607 let alice = Wallet::from_seed("alice"); 608 let bob = Wallet::from_seed("auto-once-leader"); 609 let mut allocations = BTreeMap::new(); 610 allocations.insert(alice.address().to_string(), iuna(1_000)); 611 allocations.insert(bob.address().to_string(), MICRO_IUNA); 612 let ledger = Ledger::new_with_genesis_burns( 613 allocations, 614 vec![GenesisBurn::new(bob.address(), MICRO_IUNA)], 615 10, 616 ) 617 .unwrap(); 618 assert_eq!( 619 ledger.expected_leader_for_next_block().as_deref(), 620 Some(bob.address()) 621 ); 622 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( 623 alice.clone(), 624 ledger, 625 true, 626 iuna(25), 627 DEFAULT_FEE_PER_BYTE, 628 ); 629 630 let first = node.automatic_mine_once(1); 631 assert!(first.burned.is_some()); 632 let burned = first.burned.as_ref().unwrap(); 633 assert_eq!(burned.amount(), iuna(25)); 634 assert!(burned.fee() > burned.economic_size_bytes() as u64 * DEFAULT_FEE_PER_BYTE); 635 assert!(first.block.is_none()); 636 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 637 638 let second = node.automatic_mine_once(2); 639 assert!(second.burned.is_none()); 640 assert!(second.block.is_none()); 641 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 642 } 643 644 #[test] 645 fn default_automatic_mining_does_not_burn() { 646 assert_eq!(DEFAULT_BURN_PER_BLOCK, 0); 647 648 let alice = Wallet::from_seed("alice"); 649 let mut allocations = BTreeMap::new(); 650 allocations.insert(alice.address().to_string(), 1_000); 651 let mut node = node("alice", alice.clone(), allocations); 652 653 let outcome = node.automatic_mine_once(1); 654 assert!(outcome.burned.is_none()); 655 assert!(outcome.block.is_none()); 656 assert!( 657 outcome 658 .skipped_reason 659 .as_deref() 660 .is_some_and(|reason| reason.contains("automatic mining is off")) 661 ); 662 assert_eq!(node.ledger().balance_of(alice.address()), 1_000); 663 } 664 665 #[test] 666 fn burn_per_block_can_be_set_to_zero() { 667 let alice = Wallet::from_seed("alice"); 668 let mut allocations = BTreeMap::new(); 669 allocations.insert(alice.address().to_string(), MICRO_IUNA); 670 let mut node = node("alice", alice, allocations); 671 672 let burned = node.set_burn_per_block(25).unwrap(); 673 assert!(burned.is_some()); 674 assert!(node.status().mining.automatic); 675 assert_eq!(node.status().mining.burn_per_block, 25); 676 let burned = node.set_burn_per_block(0).unwrap(); 677 assert!(burned.is_none()); 678 assert!(!node.status().mining.automatic); 679 assert_eq!(node.status().mining.burn_per_block, 0); 680 } 681 682 #[test] 683 fn automatic_burn_status_shows_configured_fee() { 684 let alice = Wallet::from_seed("auto-fee-status-alice"); 685 let mut allocations = BTreeMap::new(); 686 allocations.insert(alice.address().to_string(), 1_000); 687 let mut node = node("alice", alice, allocations); 688 689 assert_eq!(node.status().mining.automatic_burn_fee, 1); 690 691 node.set_burn_per_block(1).unwrap(); 692 assert_eq!(node.status().mining.burn_per_block, 1); 693 assert_eq!(node.status().mining.automatic_burn_fee, 1); 694 695 node.set_automatic_burn(50, 3).unwrap(); 696 assert_eq!(node.status().mining.burn_per_block, 50); 697 assert_eq!(node.status().mining.automatic_burn_fee, 3); 698 } 699 700 #[test] 701 fn automatic_mining_uses_configured_burn_fee() { 702 let alice = Wallet::from_seed("auto-fee-burn-alice"); 703 let bob = Wallet::from_seed("auto-fee-burn-leader"); 704 let mut allocations = BTreeMap::new(); 705 allocations.insert(alice.address().to_string(), MICRO_IUNA); 706 allocations.insert(bob.address().to_string(), MICRO_IUNA); 707 let ledger = Ledger::new_with_genesis_burns( 708 allocations, 709 vec![GenesisBurn::new(bob.address(), MICRO_IUNA)], 710 25, 711 ) 712 .unwrap(); 713 assert_eq!( 714 ledger.expected_leader_for_next_block().as_deref(), 715 Some(bob.address()) 716 ); 717 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(alice, ledger, true, 50, 3); 718 719 let outcome = node.automatic_mine_once(1); 720 let burned = outcome.burned.as_ref().unwrap(); 721 assert_eq!(burned.amount(), 50); 722 assert!(burned.fee() > burned.economic_size_bytes() as u64 * 3); 723 assert!(outcome.block.is_none()); 724 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 725 } 726 727 #[test] 728 fn automatic_mining_caps_burn_to_spendable_balance_after_fee() { 729 let alice = Wallet::from_seed("auto-burn-cap-alice"); 730 let bob = Wallet::from_seed("auto-burn-cap-leader"); 731 let mut allocations = BTreeMap::new(); 732 allocations.insert(alice.address().to_string(), BLOCK_REWARD); 733 allocations.insert(bob.address().to_string(), MICRO_IUNA); 734 let ledger = Ledger::new_with_genesis_burns( 735 allocations, 736 vec![GenesisBurn::new(bob.address(), MICRO_IUNA)], 737 10, 738 ) 739 .unwrap(); 740 assert_eq!( 741 ledger.expected_leader_for_next_block().as_deref(), 742 Some(bob.address()) 743 ); 744 let planning_node = NodeCore::from_ledger_with_burn_fee_and_enabled( 745 alice.clone(), 746 ledger.clone(), 747 true, 748 BLOCK_REWARD + iuna(50), 749 DEFAULT_FEE_PER_BYTE, 750 ); 751 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( 752 alice.clone(), 753 ledger, 754 true, 755 BLOCK_REWARD + iuna(50), 756 DEFAULT_FEE_PER_BYTE, 757 ); 758 759 let outcome = node.automatic_mine_once(1); 760 761 let burned = outcome.burned.as_ref().unwrap(); 762 let next_amount = burned.amount() + 1; 763 if let Ok(next_estimate) = planning_node.estimate_burn_fee(next_amount, DEFAULT_FEE_PER_BYTE) { 764 assert!(next_amount + next_estimate.fee > BLOCK_REWARD); 765 } 766 assert!(burned.fee() > burned.economic_size_bytes() as u64 * DEFAULT_FEE_PER_BYTE); 767 assert!(outcome.block.is_none()); 768 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 769 } 770 771 #[test] 772 fn automatic_mining_preserves_configured_burn_when_only_fee_is_short() { 773 let alice = Wallet::from_seed("auto-burn-exact-target-alice"); 774 let bob = Wallet::from_seed("auto-burn-exact-target-leader"); 775 let mut allocations = BTreeMap::new(); 776 allocations.insert(alice.address().to_string(), MICRO_IUNA); 777 allocations.insert(bob.address().to_string(), MICRO_IUNA); 778 let ledger = Ledger::new_with_genesis_burns( 779 allocations, 780 vec![GenesisBurn::new(bob.address(), MICRO_IUNA)], 781 10, 782 ) 783 .unwrap(); 784 assert_eq!( 785 ledger.expected_leader_for_next_block().as_deref(), 786 Some(bob.address()) 787 ); 788 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( 789 alice.clone(), 790 ledger, 791 true, 792 MICRO_IUNA, 793 DEFAULT_FEE_PER_BYTE, 794 ); 795 796 let outcome = node.automatic_mine_once(1); 797 798 let burned = outcome.burned.as_ref().unwrap(); 799 assert_eq!(burned.amount(), MICRO_IUNA); 800 assert_eq!(burned.fee(), 0); 801 assert!(outcome.block.is_none()); 802 assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); 803 } 804 805 #[test] 806 fn setting_burn_rate_after_running_at_zero_prepares_private_anchor_burn() { 807 let alice = Wallet::from_seed("alice"); 808 let bob = Wallet::from_seed("bob"); 809 let mut allocations = BTreeMap::new(); 810 allocations.insert(alice.address().to_string(), MICRO_IUNA); 811 allocations.insert(bob.address().to_string(), MICRO_IUNA); 812 813 let mut ledger = Ledger::new(allocations.clone(), 25); 814 submit_burn(&mut ledger, &alice, 1); 815 let first = ledger.mine_next_block(&alice, 1).unwrap(); 816 ledger.apply_block(first).unwrap(); 817 818 let mut alice_node = node("alice", alice.clone(), allocations); 819 alice_node 820 .receive(iuna::app::GossipEnvelope::ChainSnapshot(ledger.snapshot())) 821 .unwrap(); 822 823 let skipped = alice_node.automatic_mine_once(2); 824 assert!(skipped.burned.is_none()); 825 assert!(skipped.block.is_none()); 826 827 let burned = alice_node.set_burn_per_block(1).unwrap(); 828 829 assert!(burned.is_some()); 830 assert!(alice_node.ledger().pending().is_empty()); 831 let outcome = alice_node.automatic_mine_once(2); 832 assert!(outcome.block.is_some()); 833 } 834 835 #[test] 836 fn automatic_mining_waits_when_wallet_is_not_selected_leader() { 837 let alice = Wallet::from_seed("alice"); 838 let bob = Wallet::from_seed("bob"); 839 let mut allocations = BTreeMap::new(); 840 allocations.insert(alice.address().to_string(), 1_000); 841 allocations.insert(bob.address().to_string(), MICRO_IUNA); 842 843 let mut ledger = Ledger::new(allocations.clone(), 25); 844 submit_burn(&mut ledger, &alice, 1); 845 let first = ledger.mine_next_block(&alice, 1).unwrap(); 846 ledger.apply_block(first).unwrap(); 847 848 let mut bob_node = node("bob", bob.clone(), allocations); 849 bob_node.set_burn_per_block(10).unwrap(); 850 bob_node 851 .receive(iuna::app::GossipEnvelope::Block(ledger.chain()[1].clone())) 852 .unwrap(); 853 854 let outcome = bob_node.automatic_mine_once(2); 855 assert!(outcome.burned.is_some()); 856 assert!(outcome.block.is_none()); 857 assert!(outcome.skipped_reason.unwrap().contains(alice.address())); 858 assert_eq!(bob_node.ledger().chain().len(), 2); 859 } 860 861 #[test] 862 fn waiting_wallet_gossips_pending_burn_to_selected_leader() { 863 let alice = Wallet::from_seed("deadlock-alice"); 864 let bob = Wallet::from_seed("deadlock-bob"); 865 let mut allocations = BTreeMap::new(); 866 allocations.insert(alice.address().to_string(), iuna(1_000)); 867 allocations.insert(bob.address().to_string(), iuna(1_000)); 868 let ledger = 869 Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(bob.address(), 1)], 25) 870 .unwrap(); 871 872 assert_eq!( 873 ledger.expected_leader_for_next_block().as_deref(), 874 Some(bob.address()) 875 ); 876 let mut alice_node = NodeCore::from_ledger(alice.clone(), ledger.clone(), 1); 877 let mut bob_node = 878 NodeCore::from_ledger_with_burn_fee_and_enabled(bob.clone(), ledger, true, 1, 1); 879 880 let alice_outcome = alice_node.automatic_mine_once(1); 881 assert!(alice_outcome.burned.is_some()); 882 assert!(alice_outcome.block.is_none()); 883 assert!( 884 alice_outcome 885 .skipped_reason 886 .unwrap() 887 .contains(bob.address()) 888 ); 889 assert!(bob_node.ledger().pending().is_empty()); 890 891 for envelope in alice_node.mempool_gossip() { 892 bob_node.receive(envelope).unwrap(); 893 } 894 895 assert!(bob_node.ledger().pending().is_empty()); 896 assert_eq!(bob_node.ledger().pending_blinded_transactions().len(), 1); 897 let bob_outcome = bob_node.automatic_mine_once(1); 898 assert!(bob_outcome.skipped_reason.is_none()); 899 assert!(bob_outcome.block.is_some()); 900 } 901 902 #[test] 903 fn pow_only_node_gossips_mine_action_to_pob_only_finalizer() { 904 let alice = Wallet::from_seed("pob-only-finalizer-alice"); 905 let bob = Wallet::from_seed("pow-only-miner-bob"); 906 let mut allocations = BTreeMap::new(); 907 allocations.insert(alice.address().to_string(), 2 * MICRO_IUNA); 908 let ledger = Ledger::new_with_genesis_burns( 909 allocations, 910 vec![GenesisBurn::new(alice.address(), MICRO_IUNA)], 911 25, 912 ) 913 .unwrap(); 914 915 assert_eq!( 916 ledger.expected_leader_for_next_block().as_deref(), 917 Some(alice.address()) 918 ); 919 let mut alice_node = NodeCore::from_ledger_with_burn_fee_and_enabled( 920 alice, 921 ledger.clone(), 922 true, 923 DEFAULT_BURN_PER_BLOCK, 924 DEFAULT_FEE_PER_BYTE, 925 ); 926 let mut bob_node = NodeCore::from_ledger_with_burn_fee_and_enabled( 927 bob.clone(), 928 ledger, 929 false, 930 DEFAULT_BURN_PER_BLOCK, 931 DEFAULT_FEE_PER_BYTE, 932 ); 933 bob_node.set_pow_mining_enabled(true); 934 935 let bob_plan = (1..10_000) 936 .map(|timestamp| bob_node.prepare_automatic_mining(timestamp)) 937 .find(|plan| plan.pow_mined.is_some()) 938 .expect("B should eventually queue a mine action"); 939 let mine = bob_plan.pow_mined.expect("B should queue a mine action"); 940 assert_eq!( 941 bob_plan.skipped_reason.as_deref(), 942 Some("automatic mining is off") 943 ); 944 assert!(alice_node.ledger().pending().is_empty()); 945 946 for envelope in bob_node.drain_outbox() { 947 alice_node.receive(envelope).unwrap(); 948 } 949 950 assert!(!alice_node.ledger().pending().is_empty()); 951 assert!( 952 alice_node 953 .ledger() 954 .pending_blinded_transactions() 955 .is_empty() 956 ); 957 assert!( 958 alice_node 959 .ledger() 960 .pending() 961 .iter() 962 .any(|tx| tx.signature() == mine.signature()) 963 ); 964 } 965 966 #[test] 967 fn block_with_wrong_vdf_rounds_is_rejected() { 968 let wallet = Wallet::from_seed("alice"); 969 let mut genesis = BTreeMap::new(); 970 genesis.insert(wallet.address().to_string(), 1_000); 971 let mut ledger = 972 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 25) 973 .unwrap(); 974 submit_burn(&mut ledger, &wallet, 1); 975 976 let mut block = ledger.mine_next_block(&wallet, 1).unwrap(); 977 block.vdf_rounds = 1; 978 block.hash = block.compute_hash(); 979 block.vdf_output = run_vdf(&block.vdf_seed(), block.vdf_rounds); 980 981 assert!(ledger.apply_block(block).is_err()); 982 } 983 984 #[test] 985 fn fallback_finalizer_can_build_block_with_extra_vdf_rounds() { 986 let alice = Wallet::from_seed("fallback-finalizer-alice"); 987 let bob = Wallet::from_seed("fallback-finalizer-bob"); 988 let wallets = [&alice, &bob]; 989 let mut genesis = BTreeMap::new(); 990 genesis.insert(alice.address().to_string(), iuna(10)); 991 genesis.insert(bob.address().to_string(), iuna(10)); 992 let mut ledger = Ledger::new_with_genesis_burns( 993 genesis, 994 vec![ 995 GenesisBurn::new(alice.address(), 1), 996 GenesisBurn::new(bob.address(), 1), 997 ], 998 25, 999 ) 1000 .unwrap(); 1001 let leader = ledger.expected_leader_for_next_block().unwrap(); 1002 let fallback = wallets 1003 .into_iter() 1004 .find(|wallet| wallet.address() != leader) 1005 .unwrap(); 1006 1007 submit_burn(&mut ledger, fallback, 1); 1008 let block = ledger.mine_next_block(fallback, 1).unwrap(); 1009 1010 assert_eq!(block.miner, fallback.address()); 1011 assert_eq!(block.finalizer_rank, 1); 1012 assert_eq!(block.vdf_rounds, 50); 1013 ledger.apply_block(block).unwrap(); 1014 } 1015 1016 #[test] 1017 fn fallback_finalizer_with_primary_vdf_rounds_is_rejected() { 1018 let alice = Wallet::from_seed("fallback-rounds-alice"); 1019 let bob = Wallet::from_seed("fallback-rounds-bob"); 1020 let wallets = [&alice, &bob]; 1021 let mut genesis = BTreeMap::new(); 1022 genesis.insert(alice.address().to_string(), iuna(10)); 1023 genesis.insert(bob.address().to_string(), iuna(10)); 1024 let mut ledger = Ledger::new_with_genesis_burns( 1025 genesis, 1026 vec![ 1027 GenesisBurn::new(alice.address(), 1), 1028 GenesisBurn::new(bob.address(), 1), 1029 ], 1030 25, 1031 ) 1032 .unwrap(); 1033 let leader = ledger.expected_leader_for_next_block().unwrap(); 1034 let fallback = wallets 1035 .into_iter() 1036 .find(|wallet| wallet.address() != leader) 1037 .unwrap(); 1038 1039 submit_burn(&mut ledger, fallback, 1); 1040 let mut block = ledger.mine_next_block(fallback, 1).unwrap(); 1041 block.vdf_rounds = 25; 1042 block.vdf_output = run_vdf(&block.vdf_seed(), block.vdf_rounds); 1043 block.hash = block.compute_hash(); 1044 1045 let error = ledger.apply_block(block).unwrap_err(); 1046 assert!(format!("{error:#}").contains("block VDF rounds are invalid")); 1047 } 1048 1049 #[test] 1050 fn fallback_finalizer_unblocks_network_when_primary_does_not_publish() { 1051 let alice = Wallet::from_seed("fallback-network-alice"); 1052 let bob = Wallet::from_seed("fallback-network-bob"); 1053 let wallets = [&alice, &bob]; 1054 let mut genesis = BTreeMap::new(); 1055 genesis.insert(alice.address().to_string(), iuna(10)); 1056 genesis.insert(bob.address().to_string(), iuna(10)); 1057 let ledger = Ledger::new_with_genesis_burns( 1058 genesis, 1059 vec![ 1060 GenesisBurn::new(alice.address(), 1), 1061 GenesisBurn::new(bob.address(), 1), 1062 ], 1063 25, 1064 ) 1065 .unwrap(); 1066 let primary = ledger.expected_leader_for_next_block().unwrap(); 1067 let fallback = wallets 1068 .into_iter() 1069 .find(|wallet| wallet.address() != primary) 1070 .unwrap(); 1071 let primary_wallet = wallets 1072 .into_iter() 1073 .find(|wallet| wallet.address() == primary) 1074 .unwrap(); 1075 1076 let mut network = InMemoryNetwork::default(); 1077 network.insert( 1078 "primary", 1079 NodeCore::from_ledger( 1080 primary_wallet.clone(), 1081 ledger.clone(), 1082 DEFAULT_BURN_PER_BLOCK, 1083 ), 1084 ); 1085 network.insert( 1086 "fallback", 1087 NodeCore::from_ledger(fallback.clone(), ledger, DEFAULT_BURN_PER_BLOCK), 1088 ); 1089 1090 queue_plaintext_burn(network.node_mut("fallback").unwrap(), fallback, 1); 1091 let block = network 1092 .node_mut("fallback") 1093 .unwrap() 1094 .mine_one_at(1) 1095 .unwrap(); 1096 assert_eq!(block.finalizer_rank, 1); 1097 assert_eq!(block.miner, fallback.address()); 1098 network.deliver_until_idle().unwrap(); 1099 1100 let tip = network.node("fallback").unwrap().ledger().status().tip_hash; 1101 for id in ["primary", "fallback"] { 1102 let status = network.node(id).unwrap().ledger().status(); 1103 assert_eq!(status.height, 1, "{id} did not accept fallback block"); 1104 assert_eq!(status.tip_hash, tip, "{id} has a different tip"); 1105 } 1106 } 1107 1108 #[test] 1109 fn recovery_block_is_rejected_before_timeout() { 1110 let alice = Wallet::from_seed("recovery-before-timeout-alice"); 1111 let bob = Wallet::from_seed("recovery-before-timeout-bob"); 1112 let mut genesis = BTreeMap::new(); 1113 genesis.insert(alice.address().to_string(), iuna(10)); 1114 genesis.insert(bob.address().to_string(), iuna(10)); 1115 let mut ledger = 1116 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 25) 1117 .unwrap(); 1118 1119 submit_burn(&mut ledger, &bob, 1); 1120 let error = ledger 1121 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS - 1) 1122 .unwrap_err(); 1123 1124 assert!(format!("{error:#}").contains("recovery block is not available")); 1125 } 1126 1127 #[test] 1128 fn recovery_block_unblocks_chain_when_only_ticket_holder_stops() { 1129 let alice = Wallet::from_seed("recovery-unblocks-alice"); 1130 let bob = Wallet::from_seed("recovery-unblocks-bob"); 1131 let mut genesis = BTreeMap::new(); 1132 genesis.insert(alice.address().to_string(), iuna(10)); 1133 genesis.insert(bob.address().to_string(), iuna(10)); 1134 let mut ledger = 1135 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 25) 1136 .unwrap(); 1137 1138 assert_eq!( 1139 ledger.expected_leader_for_next_block().as_deref(), 1140 Some(alice.address()) 1141 ); 1142 submit_burn(&mut ledger, &bob, 1); 1143 assert!(ledger.mine_next_block(&bob, 1).is_err()); 1144 1145 let block = ledger 1146 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 1147 .unwrap(); 1148 1149 assert_eq!(block.miner, bob.address()); 1150 assert_eq!(block.finalizer_mode, FinalizerMode::Recovery); 1151 assert_eq!(block.finalizer_rank, 0); 1152 assert_eq!(block.vdf_rounds, 25); 1153 assert!(block.leader_proof.is_none()); 1154 ledger.apply_block(block).unwrap(); 1155 assert_eq!(ledger.status().height, 1); 1156 } 1157 1158 #[test] 1159 fn recovery_block_requires_finalizer_own_burn() { 1160 let alice = Wallet::from_seed("recovery-own-burn-alice"); 1161 let bob = Wallet::from_seed("recovery-own-burn-bob"); 1162 let mut genesis = BTreeMap::new(); 1163 genesis.insert(alice.address().to_string(), iuna(10)); 1164 genesis.insert(bob.address().to_string(), iuna(10)); 1165 let mut ledger = 1166 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 25) 1167 .unwrap(); 1168 1169 submit_burn(&mut ledger, &alice, 1); 1170 let error = ledger 1171 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 1172 .unwrap_err(); 1173 1174 assert!(format!("{error:#}").contains("burn from the finalizer")); 1175 } 1176 1177 #[test] 1178 fn recovery_block_prioritizes_finalizer_own_burn() { 1179 let alice = Wallet::from_seed("recovery-prioritizes-alice"); 1180 let bob = Wallet::from_seed("recovery-prioritizes-bob"); 1181 let mut genesis = BTreeMap::new(); 1182 genesis.insert(alice.address().to_string(), iuna(10)); 1183 genesis.insert(bob.address().to_string(), iuna(10)); 1184 let mut ledger = 1185 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 25) 1186 .unwrap(); 1187 1188 let alice_burn = ledger.build_burn(&alice, 1, 1).unwrap(); 1189 ledger.submit_transaction(alice_burn).unwrap(); 1190 let bob_burn = ledger.build_burn(&bob, 1, 0).unwrap(); 1191 let bob_burn_signature = bob_burn.signature().to_string(); 1192 ledger.submit_transaction(bob_burn).unwrap(); 1193 1194 let block = ledger 1195 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 1196 .unwrap(); 1197 1198 assert!( 1199 block 1200 .transactions 1201 .iter() 1202 .any(|tx| tx.signature() == bob_burn_signature) 1203 ); 1204 } 1205 1206 #[test] 1207 fn recovery_vdf_seed_is_bound_to_timestamp() { 1208 let alice = Wallet::from_seed("recovery-vdf-seed-alice"); 1209 let bob = Wallet::from_seed("recovery-vdf-seed-bob"); 1210 let mut genesis = BTreeMap::new(); 1211 genesis.insert(alice.address().to_string(), iuna(10)); 1212 genesis.insert(bob.address().to_string(), iuna(10)); 1213 let mut ledger = 1214 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 25) 1215 .unwrap(); 1216 1217 submit_burn(&mut ledger, &bob, 1); 1218 let mut block = ledger 1219 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 1220 .unwrap(); 1221 block.timestamp_ms += 1; 1222 block.hash = block.compute_hash(); 1223 1224 let error = ledger.apply_block(block).unwrap_err(); 1225 assert!(format!("{error:#}").contains("block VDF output is invalid")); 1226 } 1227 1228 #[test] 1229 fn fork_choice_prefers_ticket_block_over_recovery_block_at_same_height() { 1230 let alice = Wallet::from_seed("recovery-fork-alice"); 1231 let bob = Wallet::from_seed("recovery-fork-bob"); 1232 let wallets = [&alice, &bob]; 1233 let mut genesis = BTreeMap::new(); 1234 genesis.insert(alice.address().to_string(), iuna(10)); 1235 genesis.insert(bob.address().to_string(), iuna(10)); 1236 let common = Ledger::new_with_genesis_burns( 1237 genesis, 1238 vec![ 1239 GenesisBurn::new(alice.address(), 1), 1240 GenesisBurn::new(bob.address(), 1), 1241 ], 1242 25, 1243 ) 1244 .unwrap(); 1245 let leader_address = common.expected_leader_for_next_block().unwrap(); 1246 let leader = wallets 1247 .iter() 1248 .copied() 1249 .find(|wallet| wallet.address() == leader_address) 1250 .unwrap(); 1251 let recovery = wallets 1252 .into_iter() 1253 .find(|wallet| wallet.address() != leader_address) 1254 .unwrap(); 1255 1256 let mut local = common.clone(); 1257 submit_burn(&mut local, recovery, 1); 1258 let recovery_block = local 1259 .mine_recovery_block(recovery, RECOVERY_BLOCK_DELAY_MS) 1260 .unwrap(); 1261 assert_eq!(recovery_block.finalizer_mode, FinalizerMode::Recovery); 1262 local.apply_block(recovery_block).unwrap(); 1263 1264 let mut remote = common; 1265 submit_burn(&mut remote, leader, 1); 1266 let leader_block = remote.mine_next_block(leader, 1).unwrap(); 1267 assert_eq!(leader_block.finalizer_mode, FinalizerMode::Ticket); 1268 let leader_hash = leader_block.hash.clone(); 1269 remote.apply_block(leader_block).unwrap(); 1270 1271 assert!(local.extend_from_snapshot(remote.snapshot()).unwrap()); 1272 assert_eq!(local.status().tip_hash, leader_hash); 1273 } 1274 1275 #[test] 1276 fn fork_choice_prefers_primary_finalizer_over_fallback_rank() { 1277 let alice = Wallet::from_seed("fallback-fork-alice"); 1278 let bob = Wallet::from_seed("fallback-fork-bob"); 1279 let wallets = [&alice, &bob]; 1280 let mut genesis = BTreeMap::new(); 1281 genesis.insert(alice.address().to_string(), iuna(10)); 1282 genesis.insert(bob.address().to_string(), iuna(10)); 1283 let common = Ledger::new_with_genesis_burns( 1284 genesis, 1285 vec![ 1286 GenesisBurn::new(alice.address(), 1), 1287 GenesisBurn::new(bob.address(), 1), 1288 ], 1289 25, 1290 ) 1291 .unwrap(); 1292 let leader_address = common.expected_leader_for_next_block().unwrap(); 1293 let leader = wallets 1294 .iter() 1295 .copied() 1296 .find(|wallet| wallet.address() == leader_address) 1297 .unwrap(); 1298 let fallback = wallets 1299 .into_iter() 1300 .find(|wallet| wallet.address() != leader_address) 1301 .unwrap(); 1302 1303 let mut local = common.clone(); 1304 submit_burn(&mut local, fallback, 1); 1305 let fallback_block = local.mine_next_block(fallback, 1).unwrap(); 1306 assert_eq!(fallback_block.finalizer_rank, 1); 1307 local.apply_block(fallback_block).unwrap(); 1308 1309 let mut remote = common; 1310 submit_burn(&mut remote, leader, 1); 1311 let leader_block = remote.mine_next_block(leader, 1).unwrap(); 1312 assert_eq!(leader_block.finalizer_rank, 0); 1313 let leader_hash = leader_block.hash.clone(); 1314 remote.apply_block(leader_block).unwrap(); 1315 1316 assert!(local.extend_from_snapshot(remote.snapshot()).unwrap()); 1317 assert_eq!(local.status().tip_hash, leader_hash); 1318 } 1319 1320 #[test] 1321 fn vdf_solution_verifies_without_rerunning_delay() { 1322 let solution = run_vdf("test-seed", 128); 1323 1324 assert!(verify_vdf("test-seed", 128, &solution)); 1325 assert!(!verify_vdf("other-seed", 128, &solution)); 1326 assert!(!verify_vdf("test-seed", 129, &solution)); 1327 assert!(!verify_vdf("test-seed", 128, "not-a-vdf-solution")); 1328 } 1329 1330 #[test] 1331 fn vdf_rounds_retarget_toward_target_block_time() { 1332 let wallet = Wallet::from_seed("alice"); 1333 let mut genesis = BTreeMap::new(); 1334 genesis.insert(wallet.address().to_string(), 1_000); 1335 let mut ledger = Ledger::new(genesis, 100); 1336 1337 submit_burn(&mut ledger, &wallet, 1); 1338 let block1 = ledger 1339 .mine_next_block(&wallet, VDF_TARGET_BLOCK_MS) 1340 .unwrap(); 1341 assert_eq!(block1.vdf_rounds, 100); 1342 ledger.apply_block(block1).unwrap(); 1343 assert_eq!(ledger.vdf_rounds(), 100); 1344 1345 submit_burn(&mut ledger, &wallet, 1); 1346 let block2 = ledger 1347 .mine_next_block(&wallet, VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS / 2) 1348 .unwrap(); 1349 assert_eq!(block2.vdf_rounds, 100); 1350 ledger.apply_block(block2).unwrap(); 1351 assert_eq!(ledger.vdf_rounds(), 102); 1352 1353 submit_burn(&mut ledger, &wallet, 1); 1354 let block3 = ledger 1355 .mine_next_block( 1356 &wallet, 1357 VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS / 2 + VDF_TARGET_BLOCK_MS * 2, 1358 ) 1359 .unwrap(); 1360 assert_eq!(block3.vdf_rounds, 102); 1361 ledger.apply_block(block3).unwrap(); 1362 assert_eq!(ledger.vdf_rounds(), 100); 1363 } 1364 1365 #[test] 1366 fn block_timestamp_too_far_in_future_is_rejected() { 1367 let wallet = Wallet::from_seed("future-timestamp-alice"); 1368 let mut genesis = BTreeMap::new(); 1369 genesis.insert(wallet.address().to_string(), 1_000); 1370 let mut ledger = Ledger::new(genesis, 25); 1371 submit_burn(&mut ledger, &wallet, 1); 1372 1373 let far_future = unix_now_ms() 1374 .saturating_add(VDF_TARGET_BLOCK_MS) 1375 .saturating_add(1); 1376 let block = ledger.mine_next_block(&wallet, far_future).unwrap(); 1377 1378 let error = ledger.apply_block(block).unwrap_err(); 1379 1380 assert!(format!("{error:#}").contains("too far in the future")); 1381 } 1382 1383 #[test] 1384 fn genesis_wall_clock_gap_does_not_lower_vdf_rounds() { 1385 let wallet = Wallet::from_seed("genesis-gap-vdf-alice"); 1386 let mut genesis = BTreeMap::new(); 1387 genesis.insert(wallet.address().to_string(), 1_000); 1388 let mut ledger = Ledger::new(genesis, 100); 1389 let now = unix_now_ms(); 1390 let first_timestamp = now.saturating_sub(VDF_TARGET_BLOCK_MS); 1391 1392 submit_burn(&mut ledger, &wallet, 1); 1393 let block1 = ledger.mine_next_block(&wallet, first_timestamp).unwrap(); 1394 ledger.apply_block(block1).unwrap(); 1395 assert_eq!(ledger.vdf_rounds(), 100); 1396 1397 submit_burn(&mut ledger, &wallet, 1); 1398 let block2 = ledger.mine_next_block(&wallet, now).unwrap(); 1399 ledger.apply_block(block2).unwrap(); 1400 1401 assert_eq!(ledger.vdf_rounds(), 100); 1402 } 1403 1404 #[test] 1405 fn conflicting_utxo_spends_are_not_accepted_together() { 1406 let wallet = Wallet::from_seed("alice"); 1407 let mut genesis = BTreeMap::new(); 1408 genesis.insert(wallet.address().to_string(), 1_000); 1409 let mut ledger = Ledger::new(genesis, 25); 1410 1411 let first = burn_tx(&ledger, &wallet, 3); 1412 let conflicting = burn_tx(&ledger, &wallet, 4); 1413 ledger.submit_transaction(first.clone()).unwrap(); 1414 let outcome = ledger 1415 .submit_transaction_with_outcome(conflicting.clone()) 1416 .unwrap(); 1417 assert_eq!(outcome, TransactionSubmitOutcome::ConflictsWithPending); 1418 assert!(!ledger.submit_transaction(conflicting).unwrap()); 1419 assert_eq!(ledger.pending().len(), 1); 1420 1421 let block = ledger 1422 .prepare_next_block(wallet.address(), 1) 1423 .unwrap() 1424 .finish(&wallet, "test-vdf".to_string()); 1425 assert_eq!(block.transactions.len(), 1); 1426 assert!(block.transactions.contains(&first)); 1427 } 1428 1429 #[test] 1430 fn in_memory_network_syncs_nodes_without_tcp() { 1431 let alice = Wallet::from_seed("alice"); 1432 let bob = Wallet::from_seed("bob"); 1433 let mut allocations = BTreeMap::new(); 1434 allocations.insert(alice.address().to_string(), 1_000); 1435 allocations.insert(bob.address().to_string(), 1_000); 1436 1437 let mut network = InMemoryNetwork::default(); 1438 network.insert("alice", node("alice", alice.clone(), allocations.clone())); 1439 network.insert("bob", node("bob", bob.clone(), allocations)); 1440 1441 queue_plaintext_burn(network.node_mut("alice").unwrap(), &alice, 10); 1442 network.deliver_until_idle().unwrap(); 1443 assert!(network.node("bob").unwrap().ledger().pending().is_empty()); 1444 1445 network.node_mut("alice").unwrap().mine_one().unwrap(); 1446 network.deliver_until_idle().unwrap(); 1447 1448 let alice_tip = network.node("alice").unwrap().ledger().status().tip_hash; 1449 let bob_tip = network.node("bob").unwrap().ledger().status().tip_hash; 1450 assert_eq!(alice_tip, bob_tip); 1451 assert_eq!(network.node("bob").unwrap().ledger().chain().len(), 2); 1452 } 1453 1454 #[test] 1455 fn in_memory_network_delivers_transaction_to_multiple_peers() { 1456 let wallets = wallets(&["alice", "bob", "carol", "dave"]); 1457 let allocations = allocations(&wallets, MICRO_IUNA); 1458 let mut network = InMemoryNetwork::default(); 1459 1460 for (name, wallet) in ["alice", "bob", "carol", "dave"] 1461 .iter() 1462 .zip(wallets.clone()) 1463 { 1464 network.insert(*name, node(name, wallet, allocations.clone())); 1465 } 1466 1467 network.node_mut("alice").unwrap().burn(15).unwrap(); 1468 network.deliver_until_idle().unwrap(); 1469 1470 for name in ["bob", "carol", "dave"] { 1471 let ledger = network.node(name).unwrap().ledger(); 1472 assert!( 1473 ledger.pending().is_empty(), 1474 "{name} received a plaintext transaction" 1475 ); 1476 assert_eq!( 1477 ledger.pending_blinded_transactions().len(), 1478 1, 1479 "{name} did not receive alice's blinded burn" 1480 ); 1481 } 1482 } 1483 1484 #[test] 1485 fn in_memory_network_syncs_mined_block_to_multiple_peers() { 1486 let wallets = wallets(&["alice", "bob", "carol", "dave"]); 1487 let allocations = allocations(&wallets, MICRO_IUNA); 1488 let mut network = InMemoryNetwork::default(); 1489 1490 for (name, wallet) in ["alice", "bob", "carol", "dave"] 1491 .iter() 1492 .zip(wallets.clone()) 1493 { 1494 network.insert(*name, node(name, wallet, allocations.clone())); 1495 } 1496 1497 queue_plaintext_burn(network.node_mut("alice").unwrap(), &wallets[0], 10); 1498 network.deliver_until_idle().unwrap(); 1499 network.node_mut("alice").unwrap().mine_one().unwrap(); 1500 network.deliver_until_idle().unwrap(); 1501 1502 let tip = network.node("alice").unwrap().ledger().status().tip_hash; 1503 for name in ["alice", "bob", "carol", "dave"] { 1504 let ledger = network.node(name).unwrap().ledger(); 1505 assert_eq!(ledger.status().height, 1, "{name} is at the wrong height"); 1506 assert_eq!(ledger.status().tip_hash, tip, "{name} has a different tip"); 1507 assert!( 1508 ledger.pending().is_empty(), 1509 "{name} kept mined transactions" 1510 ); 1511 } 1512 } 1513 1514 #[test] 1515 fn in_memory_network_range_syncs_node_that_missed_multiple_blocks() { 1516 let alice = Wallet::from_seed("alice"); 1517 let bob = Wallet::from_seed("bob"); 1518 let wallets = vec![alice.clone(), bob]; 1519 let allocations = allocations(&wallets, 1_000); 1520 let mut network = InMemoryNetwork::default(); 1521 1522 network.insert("alice", node("alice", alice.clone(), allocations.clone())); 1523 network.insert("bob", node("bob", wallets[1].clone(), allocations)); 1524 1525 for height in 1..=5 { 1526 let alice_node = network.node_mut("alice").unwrap(); 1527 queue_plaintext_burn(alice_node, &alice, 1); 1528 alice_node 1529 .mine_one_at(height * VDF_TARGET_BLOCK_MS) 1530 .unwrap(); 1531 } 1532 assert_eq!(network.node("alice").unwrap().ledger().status().height, 5); 1533 assert_eq!(network.node("bob").unwrap().ledger().status().height, 0); 1534 1535 assert!(network.sync_node_from_peer("alice", "bob", 2).unwrap()); 1536 assert_eq!(network.node("bob").unwrap().ledger().status().height, 2); 1537 1538 assert!(network.sync_node_from_peer("alice", "bob", 10).unwrap()); 1539 let alice_tip = network.node("alice").unwrap().ledger().status().tip_hash; 1540 let bob_status = network.node("bob").unwrap().ledger().status(); 1541 assert_eq!(bob_status.height, 5); 1542 assert_eq!(bob_status.tip_hash, alice_tip); 1543 1544 network.deliver_until_idle().unwrap(); 1545 queue_plaintext_burn(network.node_mut("alice").unwrap(), &alice, 1); 1546 network.deliver_until_idle().unwrap(); 1547 network 1548 .node_mut("alice") 1549 .unwrap() 1550 .mine_one_at(6 * VDF_TARGET_BLOCK_MS) 1551 .unwrap(); 1552 network.deliver_until_idle().unwrap(); 1553 1554 let alice_tip = network.node("alice").unwrap().ledger().status().tip_hash; 1555 let bob_status = network.node("bob").unwrap().ledger().status(); 1556 assert_eq!(bob_status.height, 6); 1557 assert_eq!(bob_status.tip_hash, alice_tip); 1558 } 1559 1560 #[test] 1561 fn joined_nodes_import_transfer_block_and_blinded_burn_reveal() { 1562 let alice = Wallet::from_seed("flow-alice"); 1563 let bob = Wallet::from_seed("flow-bob"); 1564 let carol = Wallet::from_seed("flow-carol"); 1565 let mut genesis = BTreeMap::new(); 1566 genesis.insert(alice.address().to_string(), iuna(100)); 1567 let alice_ledger = Ledger::new_with_genesis_burns( 1568 genesis, 1569 vec![GenesisBurn::new(alice.address(), MICRO_IUNA)], 1570 5, 1571 ) 1572 .unwrap(); 1573 1574 let mut network = InMemoryNetwork::default(); 1575 network.insert( 1576 "a", 1577 NodeCore::from_ledger(alice.clone(), alice_ledger, DEFAULT_BURN_PER_BLOCK), 1578 ); 1579 let mut mined_by = Vec::new(); 1580 1581 for height in 1..=2 { 1582 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1583 network.deliver_until_idle().unwrap(); 1584 let block = network.node_mut("a").unwrap().mine_one_at(height).unwrap(); 1585 mined_by.push(block.miner.clone()); 1586 network.deliver_until_idle().unwrap(); 1587 } 1588 1589 let bob_ledger = Ledger::from_snapshot(network.node("a").unwrap().chain_snapshot()).unwrap(); 1590 network.insert( 1591 "b", 1592 NodeCore::from_ledger(bob.clone(), bob_ledger, DEFAULT_BURN_PER_BLOCK), 1593 ); 1594 1595 for height in 3..=4 { 1596 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1597 network.deliver_until_idle().unwrap(); 1598 let block = network.node_mut("a").unwrap().mine_one_at(height).unwrap(); 1599 mined_by.push(block.miner.clone()); 1600 network.deliver_until_idle().unwrap(); 1601 } 1602 1603 let carol_ledger = Ledger::from_snapshot(network.node("a").unwrap().chain_snapshot()).unwrap(); 1604 network.insert( 1605 "c", 1606 NodeCore::from_ledger(carol.clone(), carol_ledger, DEFAULT_BURN_PER_BLOCK), 1607 ); 1608 1609 queue_plaintext_transfer( 1610 network.node_mut("a").unwrap(), 1611 &alice, 1612 bob.address(), 1613 iuna(30), 1614 ); 1615 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1616 let block5 = network.node_mut("a").unwrap().mine_one_at(5).unwrap(); 1617 assert!( 1618 block5 1619 .transactions 1620 .iter() 1621 .any(|tx| tx.to() == Some(bob.address()) && tx.amount() == iuna(30)) 1622 ); 1623 mined_by.push(block5.miner.clone()); 1624 network.deliver_until_idle().unwrap(); 1625 1626 for id in ["a", "b", "c"] { 1627 assert_eq!( 1628 network.node(id).unwrap().ledger().status().height, 1629 5, 1630 "{id} did not import block 5" 1631 ); 1632 assert_eq!( 1633 network.node(id).unwrap().ledger().balance_of(bob.address()), 1634 iuna(30), 1635 "{id} did not apply A -> B transfer" 1636 ); 1637 } 1638 1639 let bob_burn = network.node_mut("b").unwrap().burn(10).unwrap(); 1640 network.deliver_until_idle().unwrap(); 1641 assert_eq!( 1642 network 1643 .node("a") 1644 .unwrap() 1645 .ledger() 1646 .pending_blinded_transactions() 1647 .len(), 1648 1 1649 ); 1650 1651 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1652 let commit_block = network.node_mut("a").unwrap().mine_one_at(6).unwrap(); 1653 assert_eq!(commit_block.blinded_transactions.len(), 1); 1654 assert_eq!(commit_block.blinded_transactions[0].fee, bob_burn.fee()); 1655 mined_by.push(commit_block.miner.clone()); 1656 network.deliver_until_idle().unwrap(); 1657 assert_eq!( 1658 network 1659 .node("a") 1660 .unwrap() 1661 .ledger() 1662 .pending_blinded_reveals() 1663 .len(), 1664 1 1665 ); 1666 1667 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1668 let reveal_block = network.node_mut("a").unwrap().mine_one_at(7).unwrap(); 1669 assert_eq!(reveal_block.all_blinded_reveals().len(), 1); 1670 mined_by.push(reveal_block.miner.clone()); 1671 network.deliver_until_idle().unwrap(); 1672 let revealed = revealed_blinded_transactions(&network.node("a").unwrap().chain_snapshot()) 1673 .unwrap() 1674 .into_iter() 1675 .filter(|revealed| revealed.height == 7) 1676 .collect::<Vec<_>>(); 1677 assert_eq!(revealed.len(), 1); 1678 assert!(revealed[0].transaction.is_burn(), "{revealed:?}"); 1679 assert_eq!(revealed[0].transaction.amount(), 10); 1680 1681 for height in 8..=9 { 1682 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1683 let block = network.node_mut("a").unwrap().mine_one_at(height).unwrap(); 1684 mined_by.push(block.miner.clone()); 1685 network.deliver_until_idle().unwrap(); 1686 } 1687 1688 let final_tip = network.node("a").unwrap().ledger().status().tip_hash; 1689 for id in ["a", "b", "c"] { 1690 assert_eq!(network.node(id).unwrap().ledger().status().height, 9); 1691 assert_eq!( 1692 network.node(id).unwrap().ledger().status().tip_hash, 1693 final_tip 1694 ); 1695 } 1696 let ranks = network 1697 .node("a") 1698 .unwrap() 1699 .burn_leader_ranks_for_block(10) 1700 .unwrap(); 1701 assert!( 1702 ranks.iter().any(|rank| rank.owner == bob.address() 1703 && rank.amount == 10 1704 && rank.eligible_from_height == 10), 1705 "alice={} bob={} ranks={ranks:?}", 1706 alice.address(), 1707 bob.address() 1708 ); 1709 assert!(mined_by.iter().all(|miner| miner == alice.address())); 1710 } 1711 1712 #[test] 1713 fn persisted_joined_nodes_restart_and_keep_syncing_without_tcp() { 1714 let temp = tempdir().unwrap(); 1715 let alice = Wallet::from_seed("persistent-flow-alice"); 1716 let bob = Wallet::from_seed("persistent-flow-bob"); 1717 let carol = Wallet::from_seed("persistent-flow-carol"); 1718 let mut genesis = BTreeMap::new(); 1719 genesis.insert(alice.address().to_string(), 50); 1720 let alice_ledger = 1721 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 5) 1722 .unwrap(); 1723 1724 let mut network = InMemoryNetwork::default(); 1725 network.insert( 1726 "a", 1727 NodeCore::from_ledger(alice.clone(), alice_ledger, DEFAULT_BURN_PER_BLOCK), 1728 ); 1729 1730 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1731 network.node_mut("a").unwrap().mine_one_at(1).unwrap(); 1732 network.deliver_until_idle().unwrap(); 1733 1734 let bob_store = SqliteChainStore::open(temp.path().join("bob.sqlite3")).unwrap(); 1735 bob_store 1736 .save(&network.node("a").unwrap().chain_snapshot()) 1737 .unwrap(); 1738 let bob_joined_ledger = Ledger::from_snapshot(bob_store.load().unwrap().unwrap()).unwrap(); 1739 network.insert( 1740 "b", 1741 NodeCore::from_ledger(bob.clone(), bob_joined_ledger, DEFAULT_BURN_PER_BLOCK), 1742 ); 1743 assert_eq!( 1744 network.node("b").unwrap().ledger().status().tip_hash, 1745 network.node("a").unwrap().ledger().status().tip_hash 1746 ); 1747 1748 queue_plaintext_transfer(network.node_mut("a").unwrap(), &alice, bob.address(), 10); 1749 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1750 network.deliver_until_idle().unwrap(); 1751 network.node_mut("a").unwrap().mine_one_at(2).unwrap(); 1752 network.deliver_until_idle().unwrap(); 1753 assert_eq!( 1754 network 1755 .node("b") 1756 .unwrap() 1757 .ledger() 1758 .balance_of(bob.address()), 1759 10 1760 ); 1761 1762 bob_store 1763 .save(&network.node("b").unwrap().chain_snapshot()) 1764 .unwrap(); 1765 let bob_restarted_ledger = Ledger::from_snapshot(bob_store.load().unwrap().unwrap()).unwrap(); 1766 network.insert( 1767 "b", 1768 NodeCore::from_ledger(bob.clone(), bob_restarted_ledger, DEFAULT_BURN_PER_BLOCK), 1769 ); 1770 assert_eq!( 1771 network.node("b").unwrap().ledger().status().tip_hash, 1772 network.node("a").unwrap().ledger().status().tip_hash, 1773 "restarted Bob should resume the persisted chain tip" 1774 ); 1775 1776 let carol_store = SqliteChainStore::open(temp.path().join("carol.sqlite3")).unwrap(); 1777 carol_store 1778 .save(&network.node("a").unwrap().chain_snapshot()) 1779 .unwrap(); 1780 let carol_joined_ledger = Ledger::from_snapshot(carol_store.load().unwrap().unwrap()).unwrap(); 1781 network.insert( 1782 "c", 1783 NodeCore::from_ledger(carol, carol_joined_ledger, DEFAULT_BURN_PER_BLOCK), 1784 ); 1785 1786 for height in 3..=6 { 1787 queue_plaintext_burn(network.node_mut("a").unwrap(), &alice, 1); 1788 network.node_mut("a").unwrap().mine_one_at(height).unwrap(); 1789 network.deliver_until_idle().unwrap(); 1790 } 1791 1792 let final_status = network.node("a").unwrap().ledger().status(); 1793 for id in ["b", "c"] { 1794 assert_eq!( 1795 network.node(id).unwrap().ledger().status().height, 1796 final_status.height, 1797 "{id} did not catch up after Bob restarted" 1798 ); 1799 assert_eq!( 1800 network.node(id).unwrap().ledger().status().tip_hash, 1801 final_status.tip_hash, 1802 "{id} ended on a different tip after Bob restarted" 1803 ); 1804 } 1805 1806 bob_store 1807 .save(&network.node("b").unwrap().chain_snapshot()) 1808 .unwrap(); 1809 assert_eq!( 1810 bob_store 1811 .load() 1812 .unwrap() 1813 .unwrap() 1814 .blocks 1815 .last() 1816 .unwrap() 1817 .height, 1818 final_status.height 1819 ); 1820 } 1821 1822 #[test] 1823 fn mined_block_gossip_does_not_include_full_chain_snapshot() { 1824 let alice = Wallet::from_seed("alice"); 1825 let bob = Wallet::from_seed("bob"); 1826 let wallets = vec![alice.clone(), bob.clone()]; 1827 let allocations = allocations(&wallets, MICRO_IUNA); 1828 1829 let mut alice_node = NodeCore::new(NodeConfig { 1830 wallet: alice, 1831 genesis_allocations: allocations.clone(), 1832 vdf_rounds: 10, 1833 burn_per_block: 1, 1834 burn_fee: 1, 1835 pow_mining_workers: 1, 1836 recovery_vdf_top_rank_percent: 100, 1837 }); 1838 1839 let plan = alice_node.prepare_automatic_mining(1); 1840 let burn_outbox = alice_node.drain_outbox(); 1841 assert!(burn_outbox.is_empty()); 1842 1843 let work = plan.work.unwrap(); 1844 let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds()); 1845 alice_node 1846 .complete_prepared_block(work, vdf_output) 1847 .unwrap(); 1848 let block_outbox = alice_node.drain_outbox(); 1849 1850 assert_eq!(block_outbox.len(), 1); 1851 assert!(matches!( 1852 block_outbox[0], 1853 iuna::app::GossipEnvelope::Block(_) 1854 )); 1855 } 1856 1857 #[test] 1858 fn mempool_gossip_splits_blinded_batches_at_receiver_limit() { 1859 let alice = Wallet::from_seed("mempool-batch-alice"); 1860 let bob = Wallet::from_seed("mempool-batch-bob"); 1861 let burn_wallets = (0..=TRANSACTION_BATCH_LIMIT) 1862 .map(|index| Wallet::from_seed(&format!("mempool-batch-burn-{index}"))) 1863 .collect::<Vec<_>>(); 1864 let mut wallets = vec![alice.clone(), bob.clone()]; 1865 wallets.extend(burn_wallets.clone()); 1866 let allocations = allocations(&wallets, 10_000); 1867 let mut alice_node = node("alice", alice, allocations.clone()); 1868 let mut bob_node = node("bob", bob, allocations); 1869 1870 for wallet in &burn_wallets { 1871 let tx = alice_node.ledger().build_burn(wallet, 1, 0).unwrap(); 1872 let built = alice_node 1873 .ledger() 1874 .build_blinded_transaction(wallet, tx, 20) 1875 .unwrap(); 1876 alice_node 1877 .receive_blinded_transaction(built.transaction) 1878 .unwrap(); 1879 } 1880 alice_node.drain_outbox(); 1881 1882 let gossip = alice_node.mempool_gossip(); 1883 assert_eq!(gossip.len(), 2); 1884 let total_transactions = gossip 1885 .iter() 1886 .map(|envelope| match envelope { 1887 GossipEnvelope::BlindedTransactions { transactions } => { 1888 assert!(transactions.len() <= TRANSACTION_BATCH_LIMIT); 1889 transactions.len() 1890 } 1891 other => panic!("expected blinded transaction batch, got {other:?}"), 1892 }) 1893 .sum::<usize>(); 1894 assert_eq!(total_transactions, TRANSACTION_BATCH_LIMIT + 1); 1895 1896 for envelope in gossip { 1897 bob_node.receive(envelope).unwrap(); 1898 } 1899 assert!(bob_node.ledger().pending().is_empty()); 1900 assert_eq!( 1901 bob_node.ledger().pending_blinded_transactions().len(), 1902 TRANSACTION_BATCH_LIMIT + 1 1903 ); 1904 } 1905 1906 #[test] 1907 fn received_block_is_rebroadcast_to_other_peers_without_networking() { 1908 let names = ["alice", "bob", "carol"]; 1909 let wallets = wallets(&names); 1910 let allocations = allocations(&wallets, 1_000); 1911 let alice = wallets[0].clone(); 1912 let bob = wallets[1].clone(); 1913 let carol = wallets[2].clone(); 1914 1915 let mut miner = node("alice", alice.clone(), allocations.clone()); 1916 let mut hub = node("bob", bob, allocations.clone()); 1917 let mut carol_node = node("carol", carol, allocations); 1918 1919 queue_plaintext_burn(&mut miner, &alice, 10); 1920 miner.drain_outbox(); 1921 let block = miner.mine_one_at(1).unwrap(); 1922 miner.drain_outbox(); 1923 1924 hub.receive(iuna::app::GossipEnvelope::Block(block.clone())) 1925 .unwrap(); 1926 let forwarded = hub.drain_outbox(); 1927 assert_eq!(forwarded.len(), 1); 1928 assert!(matches!(forwarded[0], iuna::app::GossipEnvelope::Block(_))); 1929 1930 for envelope in forwarded { 1931 carol_node.receive(envelope).unwrap(); 1932 } 1933 assert_eq!(carol_node.ledger().height(), 1); 1934 assert_eq!( 1935 carol_node.ledger().status().tip_hash, 1936 miner.ledger().status().tip_hash 1937 ); 1938 1939 hub.receive(iuna::app::GossipEnvelope::Block(block)) 1940 .unwrap(); 1941 assert!(hub.drain_outbox().is_empty()); 1942 } 1943 1944 #[test] 1945 fn imported_snapshot_blocks_are_rebroadcast_without_networking() { 1946 let alice = Wallet::from_seed("alice"); 1947 let bob = Wallet::from_seed("bob"); 1948 let wallets = vec![alice.clone(), bob.clone()]; 1949 let allocations = allocations(&wallets, 1_000); 1950 let mut miner = node("alice", alice.clone(), allocations.clone()); 1951 let mut hub = node("bob", bob, allocations); 1952 1953 queue_plaintext_burn(&mut miner, &alice, 1); 1954 miner.drain_outbox(); 1955 miner.mine_one_at(1).unwrap(); 1956 miner.drain_outbox(); 1957 1958 queue_plaintext_burn(&mut miner, &alice, 1); 1959 miner.drain_outbox(); 1960 miner.mine_one_at(2).unwrap(); 1961 miner.drain_outbox(); 1962 1963 hub.import_chain_snapshot(miner.chain_snapshot()).unwrap(); 1964 let outbox = hub.drain_outbox(); 1965 assert_eq!(outbox.len(), 1); 1966 match &outbox[0] { 1967 iuna::app::GossipEnvelope::Blocks { blocks } => { 1968 assert_eq!(blocks.len(), 2); 1969 assert_eq!(blocks[0].height, 1); 1970 assert_eq!(blocks[1].height, 2); 1971 } 1972 other => panic!("expected imported blocks gossip, got {other:?}"), 1973 } 1974 } 1975 1976 #[test] 1977 fn multiple_peers_can_contribute_blinded_burns_to_lottery_ranks() { 1978 let finalizer = Wallet::from_seed("blinded-burn-ranks-finalizer"); 1979 let names = ["alice", "bob", "carol", "dave"]; 1980 let wallets = wallets(&names); 1981 let mut all_wallets = vec![finalizer.clone()]; 1982 all_wallets.extend(wallets.clone()); 1983 let allocations = allocations(&all_wallets, 1_000); 1984 let ledger = Ledger::new_with_genesis_burns( 1985 allocations.clone(), 1986 vec![GenesisBurn::new(finalizer.address(), 1)], 1987 25, 1988 ) 1989 .unwrap(); 1990 let mut network = InMemoryNetwork::default(); 1991 1992 network.insert( 1993 "finalizer", 1994 NodeCore::from_ledger(finalizer.clone(), ledger.clone(), DEFAULT_BURN_PER_BLOCK), 1995 ); 1996 for (name, wallet) in names.iter().zip(wallets.clone()) { 1997 network.insert( 1998 *name, 1999 NodeCore::from_ledger(wallet, ledger.clone(), DEFAULT_BURN_PER_BLOCK), 2000 ); 2001 } 2002 2003 for ((name, wallet), amount) in names.iter().zip(wallets.iter()).zip([10, 20, 30, 40]) { 2004 let tx = network.node_mut(name).unwrap().burn(amount).unwrap(); 2005 assert!(tx.is_burn()); 2006 assert_eq!(tx.sender(), wallet.address()); 2007 } 2008 network.deliver_until_idle().unwrap(); 2009 assert_eq!( 2010 network 2011 .node("alice") 2012 .unwrap() 2013 .ledger() 2014 .pending_blinded_transactions() 2015 .len(), 2016 4 2017 ); 2018 2019 queue_plaintext_burn(network.node_mut("finalizer").unwrap(), &finalizer, 1); 2020 let commit_block = network 2021 .node_mut("finalizer") 2022 .unwrap() 2023 .mine_one_at(1) 2024 .unwrap(); 2025 assert_eq!(commit_block.blinded_transactions.len(), 4); 2026 network.deliver_until_idle().unwrap(); 2027 2028 assert_eq!( 2029 network 2030 .node("alice") 2031 .unwrap() 2032 .ledger() 2033 .pending_blinded_reveals() 2034 .len(), 2035 4 2036 ); 2037 queue_plaintext_burn(network.node_mut("finalizer").unwrap(), &finalizer, 1); 2038 network.gossip_mempools_once().unwrap(); 2039 network.deliver_until_idle().unwrap(); 2040 let reveal_block = network 2041 .node_mut("finalizer") 2042 .unwrap() 2043 .mine_one_at(2) 2044 .unwrap(); 2045 assert_eq!(reveal_block.all_blinded_reveals().len(), 4); 2046 network.deliver_until_idle().unwrap(); 2047 2048 for height in 3..=4 { 2049 queue_plaintext_burn(network.node_mut("finalizer").unwrap(), &finalizer, 1); 2050 network 2051 .node_mut("finalizer") 2052 .unwrap() 2053 .mine_one_at(height) 2054 .unwrap(); 2055 network.deliver_until_idle().unwrap(); 2056 } 2057 2058 let final_tip = network 2059 .node("finalizer") 2060 .unwrap() 2061 .ledger() 2062 .status() 2063 .tip_hash; 2064 for name in names { 2065 let ledger = network.node(name).unwrap().ledger(); 2066 assert_eq!(ledger.status().height, 4); 2067 assert_eq!(ledger.status().tip_hash, final_tip); 2068 let ranks = network 2069 .node(name) 2070 .unwrap() 2071 .burn_leader_ranks_for_block(5) 2072 .unwrap(); 2073 for (wallet, amount) in wallets.iter().zip([10, 20, 30, 40]) { 2074 assert!( 2075 ranks.iter().any(|rank| rank.owner == wallet.address() 2076 && rank.amount == amount 2077 && rank.eligible_from_height == 5), 2078 "{name} missing revealed burn ticket for {} in {ranks:?}", 2079 wallet.address() 2080 ); 2081 } 2082 } 2083 } 2084 2085 #[test] 2086 fn peer_book_tracks_multiple_peers_without_networking() { 2087 let mut peers = PeerBook::from_addresses(vec![ 2088 "127.0.0.1:9444".to_string(), 2089 "127.0.0.1:9445".to_string(), 2090 "127.0.0.1:9444".to_string(), 2091 ]); 2092 2093 peers.record_sent("127.0.0.1:9444", 2); 2094 peers.record_status("127.0.0.1:9444", 12, "tip-hash".to_string()); 2095 peers.record_error("127.0.0.1:9445", "connection refused"); 2096 peers.record_received("127.0.0.1:9555", 1); 2097 peers.record_inbound_error("127.0.0.1:56666", "invalid nonce"); 2098 2099 let mut list = peers.list(); 2100 list.sort_by(|left, right| left.address.cmp(&right.address)); 2101 assert_eq!(list.len(), 4); 2102 2103 let outbound_addresses = peers.addresses(); 2104 assert_eq!(outbound_addresses.len(), 2); 2105 assert!(outbound_addresses.contains(&"127.0.0.1:9444".to_string())); 2106 assert!(outbound_addresses.contains(&"127.0.0.1:9445".to_string())); 2107 assert!(!outbound_addresses.contains(&"127.0.0.1:56666".to_string())); 2108 2109 let sent_peer = list 2110 .iter() 2111 .find(|peer| peer.address == "127.0.0.1:9444") 2112 .unwrap(); 2113 assert_eq!(sent_peer.messages_sent, 2); 2114 assert_eq!(sent_peer.last_known_height, Some(12)); 2115 assert_eq!(sent_peer.last_known_tip_hash.as_deref(), Some("tip-hash")); 2116 assert_eq!(sent_peer.last_error, None); 2117 assert!(sent_peer.last_contact_ms.is_some()); 2118 assert!(sent_peer.last_success_ms.is_some()); 2119 assert_eq!(sent_peer.last_error_ms, None); 2120 2121 let failed_peer = list 2122 .iter() 2123 .find(|peer| peer.address == "127.0.0.1:9445") 2124 .unwrap(); 2125 assert_eq!( 2126 failed_peer.last_error.as_deref(), 2127 Some("connection refused") 2128 ); 2129 assert!(failed_peer.last_contact_ms.is_some()); 2130 assert!(failed_peer.last_error_ms.is_some()); 2131 2132 let inbound_peer = list 2133 .iter() 2134 .find(|peer| peer.address == "127.0.0.1:9555") 2135 .unwrap(); 2136 assert_eq!(inbound_peer.direction, PeerDirection::Inbound); 2137 assert_eq!(inbound_peer.messages_received, 1); 2138 assert!(inbound_peer.last_contact_ms.is_some()); 2139 assert!(inbound_peer.last_success_ms.is_some()); 2140 2141 let inbound_error = list 2142 .iter() 2143 .find(|peer| peer.address == "127.0.0.1:56666") 2144 .unwrap(); 2145 assert_eq!(inbound_error.direction, PeerDirection::Inbound); 2146 assert_eq!(inbound_error.last_error.as_deref(), Some("invalid nonce")); 2147 2148 assert!(peers.remove_peer("127.0.0.1:9445")); 2149 assert!(!peers.addresses().contains(&"127.0.0.1:9445".to_string())); 2150 assert!(!peers.remove_peer("127.0.0.1:9555")); 2151 assert!( 2152 peers 2153 .list() 2154 .iter() 2155 .any(|peer| peer.address == "127.0.0.1:9555") 2156 ); 2157 } 2158 2159 #[test] 2160 fn peer_book_reports_only_connectable_peers_as_outbound() { 2161 let mut peers = PeerBook::from_addresses(vec!["127.0.0.1:9444".to_string()]); 2162 peers.record_received("127.0.0.1:56666", 1); 2163 peers.add_discovered_peer("127.0.0.1:9445"); 2164 peers.record_inbound_misbehavior("127.0.0.1:57777", "invalid transaction"); 2165 2166 assert!(peers.is_connectable_peer("127.0.0.1:9444")); 2167 assert!(peers.is_connectable_peer("127.0.0.1:9445")); 2168 assert!(!peers.is_connectable_peer("127.0.0.1:56666")); 2169 assert!(!peers.is_connectable_peer("127.0.0.1:57777")); 2170 assert!(peers.addresses().contains(&"127.0.0.1:9445".to_string())); 2171 2172 assert!(peers.remove_peer("127.0.0.1:9444")); 2173 assert!(!peers.is_connectable_peer("127.0.0.1:9444")); 2174 } 2175 2176 #[test] 2177 fn peer_book_prunes_stale_inbound_observations() { 2178 let mut peers = PeerBook::from_addresses(vec!["127.0.0.1:9444".to_string()]); 2179 peers.record_status("127.0.0.1:9444", 1, "tip".to_string()); 2180 peers.observe_inbound_peer("127.0.0.1:56666"); 2181 peers.record_received("127.0.0.1:57777", 1); 2182 2183 assert_eq!( 2184 peers.prune_stale_inbound_peers_at(iuna::app::now_ms(), 60_000), 2185 1 2186 ); 2187 let listed = peers.list(); 2188 2189 assert!(listed.iter().any(|peer| peer.address == "127.0.0.1:9444")); 2190 assert!(listed.iter().any(|peer| peer.address == "127.0.0.1:57777")); 2191 assert!(!listed.iter().any(|peer| peer.address == "127.0.0.1:56666")); 2192 } 2193 2194 #[test] 2195 fn peer_book_bans_misbehaving_peer_temporarily_and_recovers_on_success() { 2196 let mut peers = PeerBook::from_addresses(vec!["127.0.0.1:9444".to_string()]); 2197 2198 peers.record_misbehavior_at("127.0.0.1:9444", "invalid transaction", 100); 2199 peers.record_misbehavior_at("127.0.0.1:9444", "invalid block", 200); 2200 assert!(!peers.is_banned_at("127.0.0.1:9444", 200)); 2201 assert_eq!(peers.connectable_addresses_at(200), vec!["127.0.0.1:9444"]); 2202 2203 peers.record_misbehavior_at("127.0.0.1:9444", "wrong genesis", 300); 2204 assert!(peers.is_banned_at("127.0.0.1:9444", 300)); 2205 assert!(peers.connectable_addresses_at(300).is_empty()); 2206 assert_eq!(peers.addresses(), vec!["127.0.0.1:9444"]); 2207 let banned = peers.list().pop().unwrap(); 2208 assert_eq!(banned.misbehavior_score, 3); 2209 assert_eq!(banned.ban_reason.as_deref(), Some("wrong genesis")); 2210 2211 assert!(!peers.is_banned_at("127.0.0.1:9444", 11 * 60 * 1_000)); 2212 peers.record_status("127.0.0.1:9444", 1, "tip".to_string()); 2213 let recovered = peers.list().pop().unwrap(); 2214 assert_eq!(recovered.misbehavior_score, 0); 2215 assert_eq!(recovered.banned_until_ms, None); 2216 } 2217 2218 #[test] 2219 fn peer_book_uses_median_accepted_clock_offset() { 2220 let mut peers = PeerBook::from_addresses(vec![ 2221 "127.0.0.1:9444".to_string(), 2222 "127.0.0.1:9445".to_string(), 2223 "127.0.0.1:9446".to_string(), 2224 ]); 2225 let now = 1_000_000; 2226 peers.record_status("127.0.0.1:9444", 1, "tip-a".to_string()); 2227 peers.record_status("127.0.0.1:9445", 1, "tip-b".to_string()); 2228 peers.record_status("127.0.0.1:9446", 1, "tip-c".to_string()); 2229 peers.record_clock_observation("127.0.0.1:9444", PeerDirection::Outbound, now + 1_000, now); 2230 peers.record_clock_observation("127.0.0.1:9445", PeerDirection::Outbound, now + 2_000, now); 2231 peers.record_clock_observation( 2232 "127.0.0.1:9446", 2233 PeerDirection::Outbound, 2234 now + 20 * 60 * 1_000, 2235 now, 2236 ); 2237 2238 assert_eq!(peers.network_time_offset_ms_at(now), Some(2_000)); 2239 assert_eq!(peers.adjusted_time_ms_at(now), now + 2_000); 2240 assert_eq!(peers.bad_clock_peer_count_at(now), 1); 2241 assert!(!peers.is_banned_at("127.0.0.1:9446", now)); 2242 } 2243 2244 #[test] 2245 fn peer_book_address_replacement_keeps_newest_clock_observation() { 2246 let mut peers = PeerBook::from_addresses(vec![ 2247 "seed.example:9444".to_string(), 2248 "142.132.164.59:9444".to_string(), 2249 ]); 2250 peers.record_clock_observation( 2251 "seed.example:9444", 2252 PeerDirection::Outbound, 2253 1_001_000, 2254 1_000_000, 2255 ); 2256 peers.record_clock_observation( 2257 "142.132.164.59:9444", 2258 PeerDirection::Outbound, 2259 2_002_000, 2260 2_000_000, 2261 ); 2262 2263 peers.replace_peer_address("seed.example:9444", "142.132.164.59:9444"); 2264 2265 let peer = peers.list().pop().unwrap(); 2266 assert_eq!(peer.last_clock_offset_ms, Some(2_000)); 2267 assert_eq!(peer.last_clock_observed_ms, Some(2_000_000)); 2268 } 2269 2270 #[test] 2271 fn chain_snapshot_round_trips_ledger_state() { 2272 let alice = Wallet::from_seed("alice"); 2273 let mut allocations = BTreeMap::new(); 2274 allocations.insert(alice.address().to_string(), 1_000); 2275 2276 let mut ledger = Ledger::new(allocations, 10); 2277 submit_burn(&mut ledger, &alice, 10); 2278 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2279 ledger.apply_block(block).unwrap(); 2280 2281 let restored = Ledger::from_snapshot(ledger.snapshot()).unwrap(); 2282 assert_eq!(restored.status().height, ledger.status().height); 2283 assert_eq!(restored.status().tip_hash, ledger.status().tip_hash); 2284 assert_eq!( 2285 restored.balance_of(alice.address()), 2286 ledger.balance_of(alice.address()) 2287 ); 2288 } 2289 2290 #[test] 2291 fn friend_node_can_join_snapshot_from_started_chain() { 2292 let alice = Wallet::from_seed("alice"); 2293 let bob = Wallet::from_seed("bob"); 2294 2295 let mut alice_genesis = BTreeMap::new(); 2296 alice_genesis.insert(alice.address().to_string(), 1_000); 2297 let mut alice_node = node("alice", alice.clone(), alice_genesis); 2298 alice_node 2299 .set_automatic_burn_settings(true, DEFAULT_BURN_PER_BLOCK, DEFAULT_FEE_PER_BYTE) 2300 .unwrap(); 2301 queue_plaintext_burn(&mut alice_node, &alice, 1); 2302 alice_node.automatic_mine_once(1); 2303 2304 let joined_ledger = Ledger::from_snapshot(alice_node.chain_snapshot()).unwrap(); 2305 let mut bob_node = NodeCore::from_ledger(bob.clone(), joined_ledger, DEFAULT_BURN_PER_BLOCK); 2306 2307 assert_eq!( 2308 bob_node.ledger().status().tip_hash, 2309 alice_node.ledger().status().tip_hash 2310 ); 2311 assert_eq!(bob_node.ledger().status().height, 1); 2312 assert_eq!(bob_node.ledger().balance_of(bob.address()), 0); 2313 2314 let outcome = bob_node.automatic_mine_once(2); 2315 assert!(outcome.burned.is_none()); 2316 } 2317 2318 #[test] 2319 fn running_node_rejects_snapshot_from_different_genesis() { 2320 let alice = Wallet::from_seed("alice"); 2321 let bob = Wallet::from_seed("bob"); 2322 2323 let mut alice_genesis = BTreeMap::new(); 2324 alice_genesis.insert(alice.address().to_string(), 1_000); 2325 let alice_node = node("alice", alice, alice_genesis); 2326 2327 let mut bob_genesis = BTreeMap::new(); 2328 bob_genesis.insert(bob.address().to_string(), 1_000); 2329 let mut bob_node = node("bob", bob, bob_genesis); 2330 2331 let error = bob_node 2332 .import_chain_snapshot(alice_node.chain_snapshot()) 2333 .unwrap_err(); 2334 2335 assert!(error.to_string().contains("genesis")); 2336 } 2337 2338 #[test] 2339 fn same_height_fork_snapshot_does_not_reorg() { 2340 let alice = Wallet::from_seed("alice"); 2341 let bob = Wallet::from_seed("bob"); 2342 let wallets = vec![alice.clone(), bob.clone()]; 2343 let shared_genesis = allocations(&wallets, 1_000); 2344 let base = Ledger::new_with_genesis_burns( 2345 shared_genesis, 2346 vec![GenesisBurn::new(alice.address(), 1)], 2347 1, 2348 ) 2349 .unwrap(); 2350 let mut local = base.clone(); 2351 2352 let local_first_fork_hash = mine_wallet_burn_block(&mut local, &alice, 1); 2353 let remote = fork_with_worse_vrf_block(&base, &alice, &local_first_fork_hash, 1).unwrap(); 2354 2355 let local_tip = local.status().tip_hash; 2356 assert!(!local.extend_from_snapshot(remote.snapshot()).unwrap()); 2357 assert_eq!(local.status().tip_hash, local_tip); 2358 } 2359 2360 #[test] 2361 fn fork_choice_preflight_rejects_non_matching_genesis_before_scoring() { 2362 let alice = Wallet::from_seed("preflight-genesis-alice"); 2363 let bob = Wallet::from_seed("preflight-genesis-bob"); 2364 let mut local_genesis = BTreeMap::new(); 2365 local_genesis.insert(alice.address().to_string(), 1_000); 2366 let mut remote_genesis = BTreeMap::new(); 2367 remote_genesis.insert(bob.address().to_string(), 1_000); 2368 let mut local = Ledger::new(local_genesis, 1); 2369 let mut remote = Ledger::new(remote_genesis, 1); 2370 2371 mine_wallet_burn_block(&mut local, &alice, 1); 2372 for timestamp in 1..=3 { 2373 mine_wallet_burn_block(&mut remote, &bob, timestamp); 2374 } 2375 2376 let local_tip = local.status().tip_hash; 2377 let error = local.extend_from_snapshot(remote.snapshot()).unwrap_err(); 2378 2379 assert!(error.to_string().contains("genesis")); 2380 assert_eq!(local.status().tip_hash, local_tip); 2381 } 2382 2383 #[test] 2384 fn fork_choice_preflight_rejects_invalid_fork_before_vrf_scoring() { 2385 let alice = Wallet::from_seed("preflight-invalid-alice"); 2386 let shared_genesis = allocations(std::slice::from_ref(&alice), 10_000); 2387 let mut common = Ledger::new(shared_genesis, 1); 2388 for timestamp in 1..=5 { 2389 mine_wallet_burn_block(&mut common, &alice, timestamp); 2390 } 2391 2392 let mut local = common.clone(); 2393 let local_first_fork_hash = mine_wallet_burn_block(&mut local, &alice, 6); 2394 mine_wallet_burn_block(&mut local, &alice, 7); 2395 mine_wallet_burn_block(&mut local, &alice, 8); 2396 2397 let remote = fork_with_better_vrf_block(&common, &alice, &local_first_fork_hash, 100).unwrap(); 2398 assert!(remote.chain()[6].hash < local.chain()[6].hash); 2399 let mut invalid_snapshot = remote.snapshot(); 2400 if let Some(transaction) = invalid_snapshot.blocks[6].transactions.first_mut() { 2401 match transaction { 2402 iuna::domain::Transaction::Burn { signature, .. } 2403 | iuna::domain::Transaction::Transfer { signature, .. } 2404 | iuna::domain::Transaction::Mine { signature, .. } => signature.push_str("00"), 2405 } 2406 } 2407 2408 let local_tip = local.status().tip_hash; 2409 let error = local.extend_from_snapshot(invalid_snapshot).unwrap_err(); 2410 2411 assert!(error.to_string().contains("invalid")); 2412 assert_eq!(local.status().height, 8); 2413 assert_eq!(local.status().tip_hash, local_tip); 2414 } 2415 2416 #[test] 2417 fn fork_conflict_before_last_six_blocks_is_finalized_even_if_remote_is_longer() { 2418 let alice = Wallet::from_seed("finality-alice"); 2419 let shared_genesis = allocations(std::slice::from_ref(&alice), 10_000); 2420 let mut common = Ledger::new(shared_genesis, 1); 2421 mine_wallet_burn_block(&mut common, &alice, 1); 2422 2423 let mut local = common.clone(); 2424 for timestamp in 2..=8 { 2425 let timestamp_ms = next_ticket_slot_timestamp(&local, timestamp); 2426 mine_wallet_burn_block(&mut local, &alice, timestamp_ms); 2427 } 2428 2429 let mut remote = common; 2430 for timestamp in 20..=29 { 2431 let timestamp_ms = next_ticket_slot_timestamp(&remote, timestamp); 2432 mine_wallet_burn_block(&mut remote, &alice, timestamp_ms); 2433 } 2434 2435 assert_eq!(local.status().height, 8); 2436 assert_eq!(remote.status().height, 11); 2437 let finalized_local_tip = local.status().tip_hash; 2438 2439 assert!( 2440 !local.extend_from_snapshot(remote.snapshot()).unwrap(), 2441 "forks that rewrite blocks before the last six should not be accepted" 2442 ); 2443 assert_eq!(local.status().height, 8); 2444 assert_eq!(local.status().tip_hash, finalized_local_tip); 2445 } 2446 2447 #[test] 2448 fn shorter_better_rank_fork_inside_last_six_does_not_beat_positive_quality() { 2449 let alice = Wallet::from_seed("better-vrf-alice"); 2450 let shared_genesis = allocations(std::slice::from_ref(&alice), 10_000); 2451 let mut common = Ledger::new(shared_genesis, 1); 2452 for timestamp in 1..=5 { 2453 mine_wallet_burn_block(&mut common, &alice, timestamp); 2454 } 2455 2456 let mut local = common.clone(); 2457 let local_first_fork_hash = mine_wallet_burn_block(&mut local, &alice, 6); 2458 mine_wallet_burn_block(&mut local, &alice, 7); 2459 mine_wallet_burn_block(&mut local, &alice, 8); 2460 2461 let remote = fork_with_better_vrf_block(&common, &alice, &local_first_fork_hash, 100).unwrap(); 2462 2463 assert_eq!(local.status().height, 8); 2464 assert_eq!(remote.status().height, 6); 2465 assert!(remote.status().height + 2 >= local.status().height); 2466 assert!( 2467 remote.chain()[6].hash < local.chain()[6].hash, 2468 "test setup should give the remote fork the better VRF leader score" 2469 ); 2470 let remote_tip = remote.status().tip_hash; 2471 2472 assert!( 2473 !local.extend_from_snapshot(remote.snapshot()).unwrap(), 2474 "a shorter fork should not beat greater positive chain quality" 2475 ); 2476 assert_eq!(local.status().height, 8); 2477 assert_ne!(local.status().tip_hash, remote_tip); 2478 } 2479 2480 #[test] 2481 fn better_vrf_fork_inside_last_six_loses_when_more_than_two_blocks_shorter() { 2482 let alice = Wallet::from_seed("too-short-vrf-alice"); 2483 let shared_genesis = allocations(std::slice::from_ref(&alice), 10_000); 2484 let mut common = Ledger::new(shared_genesis, 1); 2485 for timestamp in 1..=5 { 2486 mine_wallet_burn_block(&mut common, &alice, timestamp); 2487 } 2488 2489 let mut local = common.clone(); 2490 let local_first_fork_hash = mine_wallet_burn_block(&mut local, &alice, 6); 2491 mine_wallet_burn_block(&mut local, &alice, 7); 2492 mine_wallet_burn_block(&mut local, &alice, 8); 2493 mine_wallet_burn_block(&mut local, &alice, 9); 2494 2495 let remote = fork_with_better_vrf_block(&common, &alice, &local_first_fork_hash, 100).unwrap(); 2496 2497 assert_eq!(local.status().height, 9); 2498 assert_eq!(remote.status().height, 6); 2499 assert!(remote.chain()[6].hash < local.chain()[6].hash); 2500 let local_tip = local.status().tip_hash; 2501 2502 assert!( 2503 !local.extend_from_snapshot(remote.snapshot()).unwrap(), 2504 "even a better VRF fork should not win when more than two blocks shorter" 2505 ); 2506 assert_eq!(local.status().height, 9); 2507 assert_eq!(local.status().tip_hash, local_tip); 2508 } 2509 2510 #[test] 2511 fn transactions_from_abandoned_fork_blocks_return_to_mempool_after_switch() { 2512 let alice = Wallet::from_seed("reorg-alice"); 2513 let bob = Wallet::from_seed("reorg-bob"); 2514 let carol = Wallet::from_seed("reorg-carol"); 2515 let wallets = vec![alice.clone(), bob.clone(), carol.clone()]; 2516 let shared_genesis = allocations(&wallets, 10_000); 2517 let mut common = Ledger::new_with_genesis_burns( 2518 shared_genesis, 2519 vec![GenesisBurn::new(alice.address(), 1)], 2520 1, 2521 ) 2522 .unwrap(); 2523 mine_wallet_burn_block(&mut common, &alice, 1); 2524 2525 let mut local = common.clone(); 2526 let abandoned_transfer = transfer_tx(&local, &bob, carol.address(), 7); 2527 local 2528 .submit_transaction(abandoned_transfer.clone()) 2529 .unwrap(); 2530 mine_wallet_burn_block(&mut local, &alice, 2); 2531 2532 let mut remote = common; 2533 for timestamp in 20..=23 { 2534 mine_wallet_burn_block(&mut remote, &alice, timestamp); 2535 } 2536 2537 assert!(local.extend_from_snapshot(remote.snapshot()).unwrap()); 2538 assert!( 2539 local 2540 .pending() 2541 .iter() 2542 .any(|tx| tx.signature() == abandoned_transfer.signature()), 2543 "transactions mined only on the abandoned fork should return to the mempool" 2544 ); 2545 } 2546 2547 #[test] 2548 fn longer_valid_fork_snapshot_reorgs_and_preserves_local_transactions() { 2549 let alice = Wallet::from_seed("alice"); 2550 let bob = Wallet::from_seed("bob"); 2551 let wallets = vec![alice.clone(), bob.clone()]; 2552 let shared_genesis = allocations(&wallets, 1_000); 2553 let mut local = Ledger::new_with_genesis_burns( 2554 shared_genesis.clone(), 2555 vec![GenesisBurn::new(alice.address(), 1)], 2556 1, 2557 ) 2558 .unwrap(); 2559 let mut remote = Ledger::new_with_genesis_burns( 2560 shared_genesis, 2561 vec![GenesisBurn::new(alice.address(), 1)], 2562 1, 2563 ) 2564 .unwrap(); 2565 2566 let local_burn = burn_tx(&local, &alice, 1); 2567 local.submit_transaction(local_burn.clone()).unwrap(); 2568 let local_block = local.mine_next_block(&alice, 1).unwrap(); 2569 local.apply_block(local_block).unwrap(); 2570 let local_transfer = transfer_tx(&local, &bob, alice.address(), 5); 2571 local.submit_transaction(local_transfer.clone()).unwrap(); 2572 2573 submit_burn(&mut remote, &alice, 1); 2574 let remote_block_1 = remote.mine_next_block(&alice, 1).unwrap(); 2575 remote.apply_block(remote_block_1).unwrap(); 2576 submit_burn(&mut remote, &alice, 1); 2577 let remote_block_2 = remote.mine_next_block(&alice, 2).unwrap(); 2578 remote.apply_block(remote_block_2).unwrap(); 2579 2580 let remote_tip = remote.status().tip_hash; 2581 assert!(local.extend_from_snapshot(remote.snapshot()).unwrap()); 2582 assert_eq!(local.status().height, 2); 2583 assert_eq!(local.status().tip_hash, remote_tip); 2584 assert!( 2585 local 2586 .pending() 2587 .iter() 2588 .any(|tx| tx.signature() == local_transfer.signature()) 2589 ); 2590 } 2591 2592 #[test] 2593 fn node_receives_chain_snapshot_envelope_when_joining_without_tcp() { 2594 let alice = Wallet::from_seed("alice"); 2595 let bob = Wallet::from_seed("bob"); 2596 2597 let wallets = vec![alice.clone(), bob.clone()]; 2598 let shared_genesis = allocations(&wallets, 1_000); 2599 let mut alice_node = node("alice", alice.clone(), shared_genesis.clone()); 2600 queue_plaintext_burn(&mut alice_node, &alice, 1); 2601 alice_node.mine_one().unwrap(); 2602 2603 let mut bob_node = node("bob", bob, shared_genesis); 2604 2605 bob_node 2606 .receive(iuna::app::GossipEnvelope::ChainSnapshot( 2607 alice_node.chain_snapshot(), 2608 )) 2609 .unwrap(); 2610 2611 assert_eq!( 2612 bob_node.ledger().status().tip_hash, 2613 alice_node.ledger().status().tip_hash 2614 ); 2615 }