tests.rs (107410B)
1 use super::*; 2 3 #[test] 4 fn target_block_time_is_five_minutes() { 5 assert_eq!(VDF_TARGET_BLOCK_MS, 5 * 60 * 1_000); 6 } 7 8 fn test_utxo_outpoint(index: usize) -> OutPoint { 9 OutPoint { 10 txid: format!("{index:064x}"), 11 index: 0, 12 } 13 } 14 15 fn named_test_outpoint(name: &str) -> OutPoint { 16 OutPoint { 17 txid: hex_hash(format!("test-utxo:{name}")), 18 index: 0, 19 } 20 } 21 22 fn ledger_with_wallet_utxos(wallet: &Wallet, amounts: &[Amount]) -> Ledger { 23 let mut ledger = Ledger::new(BTreeMap::new(), 1); 24 ledger.utxos = amounts 25 .iter() 26 .enumerate() 27 .map(|(index, amount)| { 28 ( 29 test_utxo_outpoint(index), 30 TxOutput { 31 address: wallet.address().to_string(), 32 amount: *amount, 33 }, 34 ) 35 }) 36 .collect(); 37 ledger 38 } 39 40 fn pending_balances(ledger: &Ledger) -> BTreeMap<String, Amount> { 41 balances_from_utxos(&ledger.utxos_after_valid_pending().unwrap()) 42 } 43 44 fn ledger_with_allocation(wallet: &Wallet, amount: Amount) -> Ledger { 45 let mut genesis = BTreeMap::new(); 46 genesis.insert(wallet.address().to_string(), amount); 47 Ledger::new(genesis, 1) 48 } 49 50 fn mine_burn_block_with_mines(ledger: &mut Ledger, wallet: &Wallet, mine_actions: usize) { 51 let burn = ledger.build_burn(wallet, MICRO_IUNA, 0).unwrap(); 52 ledger.submit_transaction(burn).unwrap(); 53 for _ in 0..mine_actions { 54 let mine = ledger.build_mine(wallet.address()).unwrap(); 55 ledger.submit_transaction(mine).unwrap(); 56 } 57 let block = ledger.mine_next_block(wallet, ledger.height() + 1).unwrap(); 58 ledger.apply_block(block).unwrap(); 59 } 60 61 fn test_mine_with_salt(ledger: &Ledger, recipient: &str, salt: u64) -> Transaction { 62 let anchor = ledger.tip().hash.clone(); 63 let difficulty_bits = ledger.current_mine_difficulty_bits(); 64 for nonce in 0..u64::MAX { 65 let signature = mine_signature(recipient, &anchor, salt, nonce, difficulty_bits); 66 if hash_meets_difficulty(&signature, difficulty_bits) { 67 return Transaction::Mine { 68 recipient: recipient.to_string(), 69 anchor, 70 salt, 71 nonce, 72 difficulty_bits, 73 proof_header: None, 74 signature, 75 }; 76 } 77 } 78 panic!("test should find a valid mine action"); 79 } 80 81 fn apply_preverified_burn_block_at( 82 ledger: &mut Ledger, 83 wallet: &Wallet, 84 timestamp_ms: u64, 85 ) -> Block { 86 let burn = ledger.build_burn(wallet, 1, 0).unwrap(); 87 ledger.submit_transaction(burn).unwrap(); 88 let work = ledger 89 .prepare_next_block(wallet.address(), timestamp_ms) 90 .unwrap(); 91 let block = work.finish(wallet, "preverified-vdf".to_string()); 92 ledger 93 .apply_preverified_block_at(block.clone(), u64::MAX) 94 .unwrap(); 95 block 96 } 97 98 fn apply_preverified_burn_block_with_mines( 99 ledger: &mut Ledger, 100 wallet: &Wallet, 101 mine_actions: usize, 102 ) { 103 let burn = ledger.build_burn(wallet, MICRO_IUNA, 0).unwrap(); 104 ledger.submit_transaction(burn).unwrap(); 105 let timestamp_ms = ledger 106 .tip() 107 .timestamp_ms 108 .saturating_add(VDF_TARGET_BLOCK_MS); 109 let mut block = ledger 110 .prepare_next_block(wallet.address(), timestamp_ms) 111 .unwrap() 112 .finish(wallet, "preverified-vdf".to_string()); 113 for salt in 0..mine_actions { 114 block.transactions.push(test_mine_with_salt( 115 ledger, 116 wallet.address(), 117 salt as u64 + 1, 118 )); 119 } 120 block.reward = fee_reward(&block.transactions).unwrap(); 121 block.hash = block.compute_hash(); 122 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 123 } 124 125 fn vdf_retarget_sample_block( 126 timestamp_ms: u64, 127 finalizer_mode: FinalizerMode, 128 finalizer_rank: u32, 129 ) -> Block { 130 Block { 131 height: 1, 132 prev_hash: String::new(), 133 timestamp_ms, 134 miner: String::new(), 135 finalizer_mode, 136 finalizer_rank, 137 reward: 0, 138 vdf_rounds: 1, 139 vdf_output: String::new(), 140 leader_proof: None, 141 blinded_transactions: Vec::new(), 142 reveal_bundle_section: RevealBundleSection::default(), 143 transactions: Vec::new(), 144 hash: String::new(), 145 } 146 } 147 148 const TEST_BURN_AMOUNT: Amount = MICRO_IUNA / 10; 149 150 fn unsigned_mine(ledger: &Ledger, recipient: &str) -> Transaction { 151 let anchor = ledger.tip().hash.clone(); 152 let difficulty_bits = ledger.current_mine_difficulty_bits(); 153 for nonce in 0..u64::MAX { 154 let salt = 1; 155 let signature = mine_signature(recipient, &anchor, salt, nonce, difficulty_bits); 156 if hash_meets_difficulty(&signature, difficulty_bits) { 157 return Transaction::Mine { 158 recipient: recipient.to_string(), 159 anchor, 160 salt, 161 nonce, 162 difficulty_bits, 163 proof_header: None, 164 signature, 165 }; 166 } 167 } 168 panic!("expected to find mine proof"); 169 } 170 171 fn wallet_for_address<'a>(wallets: &'a [Wallet], address: &str) -> &'a Wallet { 172 wallets 173 .iter() 174 .find(|wallet| wallet.address() == address) 175 .unwrap_or_else(|| panic!("missing wallet for address {address}")) 176 } 177 178 fn ledger_with_finalizers( 179 finalizers: &[Wallet], 180 extra_allocations: &[(&Wallet, Amount)], 181 ) -> Ledger { 182 let mut allocations = BTreeMap::new(); 183 for wallet in finalizers { 184 allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA); 185 } 186 for (wallet, amount) in extra_allocations { 187 allocations.insert(wallet.address().to_string(), *amount); 188 } 189 Ledger::new_with_genesis_burns( 190 allocations, 191 finalizers 192 .iter() 193 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) 194 .collect(), 195 1, 196 ) 197 .unwrap() 198 } 199 200 fn mine_preverified_as_next_leader( 201 ledger: &mut Ledger, 202 wallets: &[Wallet], 203 timestamp_ms: u64, 204 ) -> Block { 205 let leader = ledger.expected_leader_for_next_block().unwrap(); 206 let wallet = wallet_for_address(wallets, &leader); 207 let prepared = ledger 208 .prepare_next_block(wallet.address(), timestamp_ms) 209 .unwrap(); 210 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 211 ledger 212 .apply_preverified_block_at(block.clone(), u64::MAX) 213 .unwrap(); 214 block 215 } 216 217 fn mine_preverified_as_next_leader_with_reveal_bundles( 218 ledger: &mut Ledger, 219 wallets: &[Wallet], 220 timestamp_ms: u64, 221 ) -> Block { 222 let block = 223 prepare_preverified_as_next_leader_with_reveal_bundles(ledger, wallets, timestamp_ms); 224 ledger 225 .apply_preverified_block_at(block.clone(), u64::MAX) 226 .unwrap(); 227 block 228 } 229 230 fn prepare_preverified_as_next_leader_with_reveal_bundles( 231 ledger: &Ledger, 232 wallets: &[Wallet], 233 timestamp_ms: u64, 234 ) -> Block { 235 let bundles = ledger 236 .reveal_committee_for_next_block() 237 .into_iter() 238 .filter_map(|member| { 239 let wallet = wallet_for_address(wallets, &member.owner); 240 ledger.build_reveal_bundle(wallet).unwrap() 241 }) 242 .collect::<Vec<_>>(); 243 let leader = ledger.expected_leader_for_next_block().unwrap(); 244 let wallet = wallet_for_address(wallets, &leader); 245 let prepared = ledger 246 .prepare_next_block_with_reveal_bundles(wallet.address(), timestamp_ms, bundles) 247 .unwrap(); 248 prepared.finish(wallet, "preverified-vdf".to_string()) 249 } 250 251 fn queue_next_leader_burn(ledger: &mut Ledger, wallets: &[Wallet]) { 252 let leader = ledger.expected_leader_for_next_block().unwrap(); 253 let wallet = wallet_for_address(wallets, &leader); 254 let burn = ledger.build_burn(wallet, 1, 0).unwrap(); 255 ledger.submit_transaction(burn).unwrap(); 256 } 257 258 fn transfer_with_extra_zero_outputs( 259 ledger: &Ledger, 260 wallet: &Wallet, 261 to: &str, 262 amount: Amount, 263 fee: Amount, 264 extra_outputs: usize, 265 ) -> Transaction { 266 let required = amount.checked_add(fee).unwrap(); 267 let (inputs, input_total) = ledger.select_inputs(wallet.address(), required).unwrap(); 268 let mut outputs = vec![TxOutput { 269 address: to.to_string(), 270 amount, 271 }]; 272 outputs.extend((0..extra_outputs).map(|_| TxOutput { 273 address: to.to_string(), 274 amount: 0, 275 })); 276 let change = input_total - required; 277 if change > 0 { 278 outputs.push(TxOutput { 279 address: wallet.address().to_string(), 280 amount: change, 281 }); 282 } 283 UnsignedUtxoTransaction::Transfer { 284 inputs, 285 outputs, 286 fee, 287 } 288 .sign(wallet) 289 } 290 291 #[test] 292 fn wallet_utxos_only_include_outputs_owned_by_address() { 293 let alice = Wallet::from_seed("wallet-utxos-alice"); 294 let bob = Wallet::from_seed("wallet-utxos-bob"); 295 let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3]); 296 ledger.utxos.insert( 297 named_test_outpoint("bob"), 298 TxOutput { 299 address: bob.address().to_string(), 300 amount: 5, 301 }, 302 ); 303 304 let alice_utxos = ledger.utxos_for_address(alice.address()); 305 let total = alice_utxos 306 .iter() 307 .map(|(_, output)| output.amount) 308 .sum::<Amount>(); 309 310 assert_eq!(alice_utxos.len(), 2); 311 assert_eq!(total, ledger.balance_of(alice.address())); 312 assert!( 313 alice_utxos 314 .iter() 315 .all(|(_, output)| output.address == alice.address()) 316 ); 317 } 318 319 #[test] 320 fn transfer_combines_multiple_small_utxos_to_cover_amount_and_fee() { 321 let alice = Wallet::from_seed("combine-small-utxos-alice"); 322 let bob = Wallet::from_seed("combine-small-utxos-bob"); 323 let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 1]); 324 325 let tx = ledger.build_transfer(&alice, bob.address(), 2, 1).unwrap(); 326 327 let Transaction::Transfer { 328 inputs, 329 outputs, 330 fee, 331 .. 332 } = &tx 333 else { 334 panic!("expected transfer"); 335 }; 336 assert_eq!(inputs.len(), 3); 337 assert_eq!(*fee, 1); 338 assert_eq!( 339 outputs, 340 &[TxOutput { 341 address: bob.address().to_string(), 342 amount: 2 343 }] 344 ); 345 346 ledger.submit_transaction(tx).unwrap(); 347 let balances = pending_balances(&ledger); 348 assert_eq!( 349 balances.get(alice.address()).copied().unwrap_or_default(), 350 0 351 ); 352 assert_eq!(balances.get(bob.address()).copied().unwrap_or_default(), 2); 353 } 354 355 #[test] 356 fn transfer_returns_change_when_combined_utxos_exceed_payment() { 357 let alice = Wallet::from_seed("combine-change-alice"); 358 let bob = Wallet::from_seed("combine-change-bob"); 359 let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); 360 361 let tx = ledger.build_transfer(&alice, bob.address(), 3, 0).unwrap(); 362 363 let Transaction::Transfer { 364 inputs, outputs, .. 365 } = &tx 366 else { 367 panic!("expected transfer"); 368 }; 369 assert_eq!(inputs.len(), 3); 370 assert_eq!( 371 outputs, 372 &[ 373 TxOutput { 374 address: bob.address().to_string(), 375 amount: 3 376 }, 377 TxOutput { 378 address: alice.address().to_string(), 379 amount: 1 380 } 381 ] 382 ); 383 384 ledger.submit_transaction(tx).unwrap(); 385 let balances = pending_balances(&ledger); 386 assert_eq!(balances.get(alice.address()).copied(), Some(1)); 387 assert_eq!(balances.get(bob.address()).copied(), Some(3)); 388 } 389 390 #[test] 391 fn transaction_economic_size_uses_compact_canonical_fields() { 392 let alice = Wallet::from_seed("economic-size-alice"); 393 let bob = Wallet::from_seed("economic-size-bob"); 394 let ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); 395 let selected = vec![ 396 test_utxo_outpoint(0), 397 test_utxo_outpoint(1), 398 test_utxo_outpoint(2), 399 ]; 400 401 let tx = ledger 402 .build_transfer_with_inputs(&alice, bob.address(), 3, 0, &selected) 403 .unwrap(); 404 405 assert!(tx.economic_size_bytes() < tx.serialized_size_bytes().unwrap()); 406 assert_eq!( 407 tx.economic_size_bytes(), 408 1 + 1 + (3 * (32 + 1 + 32)) + 1 + (2 * (32 + 1)) + 1 + 64 409 ); 410 } 411 412 #[test] 413 fn blinded_fee_rate_size_uses_visible_envelope_bytes() { 414 let alice = Wallet::from_seed("blinded-fee-size-alice"); 415 let ledger = ledger_with_wallet_utxos(&alice, &[10]); 416 let built = ledger 417 .build_blinded_burn(&alice, 1, 2, ledger.height() + 4) 418 .unwrap(); 419 420 assert_eq!( 421 built.transaction.fee_rate_size_bytes(), 422 built.transaction.serialized_size_bytes().unwrap() 423 ); 424 assert!(built.transaction.fee_rate_size_bytes() > built.transaction.encrypted_size as usize); 425 } 426 427 #[test] 428 fn blinded_fee_split_burns_rounding_dust() { 429 let committer = Wallet::from_seed("blinded-split-committer"); 430 let executor = Wallet::from_seed("blinded-split-executor"); 431 let commitment = "01".repeat(32); 432 let active = ActiveBlindedTransaction { 433 transaction: BlindedTransaction { 434 commitment: commitment.clone(), 435 inputs: Vec::new(), 436 fee: 1, 437 encrypted_size: 1, 438 expires_at_height: 2, 439 nonce: "02".repeat(BLINDED_NONCE_BYTES), 440 ciphertext: "03".to_string(), 441 payload_hash: "04".repeat(32), 442 }, 443 locked_outputs: Vec::new(), 444 included_height: 1, 445 included_by: committer.address().to_string(), 446 }; 447 let transaction = Transaction::Transfer { 448 inputs: Vec::new(), 449 outputs: Vec::new(), 450 fee: 1, 451 signature: String::new(), 452 }; 453 let mut utxos = BTreeMap::new(); 454 455 credit_blinded_fee_outputs( 456 &mut utxos, 457 &active, 458 executor.address(), 459 &transaction, 460 &[], 461 3, 462 false, 463 ) 464 .unwrap(); 465 466 assert!(!utxos.contains_key(&blinded_committer_fee_outpoint(&commitment))); 467 assert!(!utxos.contains_key(&blinded_executor_fee_outpoint(&commitment))); 468 } 469 470 #[test] 471 fn blinded_fee_split_pays_no_reveal_finalizer_without_signed_reveal_lists() { 472 let committer = Wallet::from_seed("blinded-no-list-committer"); 473 let executor = Wallet::from_seed("blinded-no-list-executor"); 474 let commitment = "06".repeat(32); 475 let active = ActiveBlindedTransaction { 476 transaction: BlindedTransaction { 477 commitment: commitment.clone(), 478 inputs: Vec::new(), 479 fee: 100, 480 encrypted_size: 1, 481 expires_at_height: 2, 482 nonce: "02".repeat(BLINDED_NONCE_BYTES), 483 ciphertext: "03".to_string(), 484 payload_hash: "04".repeat(32), 485 }, 486 locked_outputs: Vec::new(), 487 included_height: 1, 488 included_by: committer.address().to_string(), 489 }; 490 let transaction = Transaction::Transfer { 491 inputs: Vec::new(), 492 outputs: Vec::new(), 493 fee: 100, 494 signature: String::new(), 495 }; 496 let mut utxos = BTreeMap::new(); 497 498 credit_blinded_fee_outputs( 499 &mut utxos, 500 &active, 501 executor.address(), 502 &transaction, 503 &[], 504 3, 505 false, 506 ) 507 .unwrap(); 508 509 assert_eq!( 510 utxos.get(&blinded_committer_fee_outpoint(&commitment)), 511 Some(&TxOutput { 512 address: committer.address().to_string(), 513 amount: 35, 514 }) 515 ); 516 assert!(!utxos.contains_key(&blinded_executor_fee_outpoint(&commitment))); 517 } 518 519 #[test] 520 fn blinded_fee_split_pays_committer_executor_and_reveal_bundle_signers() { 521 let committer = Wallet::from_seed("blinded-scale-committer"); 522 let executor = Wallet::from_seed("blinded-scale-executor"); 523 let signer_a = Wallet::from_seed("blinded-scale-signer-a"); 524 let signer_b = Wallet::from_seed("blinded-scale-signer-b"); 525 let commitment = "05".repeat(32); 526 let active = ActiveBlindedTransaction { 527 transaction: BlindedTransaction { 528 commitment: commitment.clone(), 529 inputs: Vec::new(), 530 fee: 7, 531 encrypted_size: 1, 532 expires_at_height: 2, 533 nonce: "02".repeat(BLINDED_NONCE_BYTES), 534 ciphertext: "03".to_string(), 535 payload_hash: "04".repeat(32), 536 }, 537 locked_outputs: Vec::new(), 538 included_height: 1, 539 included_by: committer.address().to_string(), 540 }; 541 let transaction = Transaction::Transfer { 542 inputs: Vec::new(), 543 outputs: Vec::new(), 544 fee: 100, 545 signature: String::new(), 546 }; 547 let mut utxos = BTreeMap::new(); 548 let signatures = vec![ 549 RevealBundleSignature { 550 slot: 0, 551 member: signer_a.address().to_string(), 552 signature: "11".repeat(SIGNATURE_BYTES), 553 }, 554 RevealBundleSignature { 555 slot: 2, 556 member: signer_b.address().to_string(), 557 signature: "22".repeat(SIGNATURE_BYTES), 558 }, 559 ]; 560 561 credit_blinded_fee_outputs( 562 &mut utxos, 563 &active, 564 executor.address(), 565 &transaction, 566 &signatures, 567 3, 568 false, 569 ) 570 .unwrap(); 571 572 assert_eq!( 573 utxos.get(&blinded_committer_fee_outpoint(&commitment)), 574 Some(&TxOutput { 575 address: committer.address().to_string(), 576 amount: 35, 577 }) 578 ); 579 assert_eq!( 580 utxos.get(&blinded_executor_fee_outpoint(&commitment)), 581 Some(&TxOutput { 582 address: executor.address().to_string(), 583 amount: 23, 584 }) 585 ); 586 assert_eq!( 587 utxos.get(&blinded_reveal_bundle_signer_fee_outpoint(&commitment, 0)), 588 Some(&TxOutput { 589 address: signer_a.address().to_string(), 590 amount: 10, 591 }) 592 ); 593 assert_eq!( 594 utxos.get(&blinded_reveal_bundle_signer_fee_outpoint(&commitment, 2)), 595 Some(&TxOutput { 596 address: signer_b.address().to_string(), 597 amount: 10, 598 }) 599 ); 600 } 601 602 #[test] 603 fn blinded_reveal_finalizer_fee_scales_by_available_reveal_bundle_slots() { 604 let fee = 300_000; 605 let full_share = blinded_fee_share(fee, BLINDED_REVEAL_FINALIZER_FEE_BPS); 606 607 assert_eq!(blinded_reveal_finalizer_fee(fee, 0, 3), 0); 608 assert_eq!(blinded_reveal_finalizer_fee(fee, 1, 3), full_share / 3); 609 assert_eq!(blinded_reveal_finalizer_fee(fee, 1, 2), full_share / 2); 610 assert_eq!(blinded_reveal_finalizer_fee(fee, 1, 1), full_share); 611 assert_eq!(blinded_reveal_finalizer_fee(fee, 2, 3), full_share * 2 / 3); 612 assert_eq!(blinded_reveal_finalizer_fee(fee, 3, 3), full_share); 613 assert_eq!(blinded_reveal_finalizer_fee(fee, 4, 3), full_share); 614 } 615 616 #[test] 617 fn reveal_fee_attribution_uses_reveal_mask_from_height_750() { 618 let alice = Wallet::from_seed("mask-fee-alice"); 619 let bob = Wallet::from_seed("mask-fee-bob"); 620 let carol = Wallet::from_seed("mask-fee-carol"); 621 let reveal = MaskedBlindedReveal { 622 reveal: BlindedReveal { 623 commitment: hex_hash("mask-fee-reveal"), 624 key: "00".repeat(BLINDED_KEY_BYTES), 625 }, 626 bundle_mask: 0b0000_0101, 627 }; 628 let section = RevealBundleSection { 629 signatures: vec![ 630 RevealBundleSignature { 631 slot: 0, 632 member: alice.address().to_string(), 633 signature: "11".repeat(SIGNATURE_BYTES), 634 }, 635 RevealBundleSignature { 636 slot: 1, 637 member: bob.address().to_string(), 638 signature: "22".repeat(SIGNATURE_BYTES), 639 }, 640 RevealBundleSignature { 641 slot: 2, 642 member: carol.address().to_string(), 643 signature: "33".repeat(SIGNATURE_BYTES), 644 }, 645 ], 646 reveals: vec![reveal.clone()], 647 }; 648 649 let legacy_signers = 650 reveal_fee_signatures_for_height(REVEAL_FEE_MASK_ATTRIBUTION_HEIGHT - 1, §ion, &reveal); 651 assert_eq!(legacy_signers.len(), 3); 652 assert_eq!( 653 reveal_fee_bundle_count_for_height( 654 REVEAL_FEE_MASK_ATTRIBUTION_HEIGHT - 1, 655 §ion, 656 &reveal 657 ), 658 3 659 ); 660 661 let masked_signers = 662 reveal_fee_signatures_for_height(REVEAL_FEE_MASK_ATTRIBUTION_HEIGHT, §ion, &reveal); 663 assert_eq!( 664 masked_signers 665 .iter() 666 .map(|signature| signature.member.as_str()) 667 .collect::<Vec<_>>(), 668 vec![alice.address(), carol.address()] 669 ); 670 assert_eq!( 671 reveal_fee_bundle_count_for_height(REVEAL_FEE_MASK_ATTRIBUTION_HEIGHT, §ion, &reveal), 672 2 673 ); 674 } 675 676 #[test] 677 fn transfer_rejects_invalid_recipient_address() { 678 let alice = Wallet::from_seed("invalid-transfer-recipient-alice"); 679 let ledger = ledger_with_wallet_utxos(&alice, &[10]); 680 681 let error = ledger.build_transfer(&alice, "aa", 1, 0).unwrap_err(); 682 683 assert!(format!("{error:#}").contains("invalid transfer recipient address")); 684 } 685 686 #[test] 687 fn mine_rejects_invalid_recipient_address_before_pow() { 688 let ledger = Ledger::new(BTreeMap::new(), 1); 689 690 let error = ledger.build_mine("aa").unwrap_err(); 691 692 assert!(format!("{error:#}").contains("invalid mine recipient address")); 693 } 694 695 #[test] 696 fn mine_search_respects_nonce_attempt_limit() { 697 let alice = Wallet::from_seed("bounded-mine-search-alice"); 698 let ledger = Ledger::new(BTreeMap::new(), 1); 699 700 let outcome = ledger.search_mine(alice.address(), 1, 0, 0).unwrap(); 701 702 assert!(outcome.transaction.is_none()); 703 assert_eq!(outcome.next_nonce, 0); 704 assert_eq!(outcome.attempts, 0); 705 } 706 707 #[test] 708 fn mempool_rejects_invalid_input_outpoint_id() { 709 let alice = Wallet::from_seed("invalid-outpoint-alice"); 710 let bob = Wallet::from_seed("invalid-outpoint-bob"); 711 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 712 let unsigned = UnsignedUtxoTransaction::Transfer { 713 inputs: vec![UnsignedTxInput { 714 outpoint: OutPoint { 715 txid: "aa".to_string(), 716 index: 0, 717 }, 718 owner: alice.address().to_string(), 719 }], 720 outputs: vec![TxOutput { 721 address: bob.address().to_string(), 722 amount: 1, 723 }], 724 fee: 0, 725 }; 726 let transaction = unsigned.sign(&alice); 727 728 let error = ledger.submit_transaction(transaction).unwrap_err(); 729 730 assert!(format!("{error:#}").contains("invalid input outpoint txid")); 731 assert!(ledger.pending().is_empty()); 732 } 733 734 #[test] 735 fn missing_input_transaction_goes_to_orphan_pool_not_pending_mempool() { 736 let alice = Wallet::from_seed("missing-input-orphan-alice"); 737 let bob = Wallet::from_seed("missing-input-orphan-bob"); 738 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 739 let transaction = UnsignedUtxoTransaction::Transfer { 740 inputs: vec![UnsignedTxInput { 741 outpoint: OutPoint { 742 txid: hex_hash("missing-input-orphan"), 743 index: 0, 744 }, 745 owner: alice.address().to_string(), 746 }], 747 outputs: vec![TxOutput { 748 address: bob.address().to_string(), 749 amount: 1, 750 }], 751 fee: 0, 752 } 753 .sign(&alice); 754 755 let outcome = ledger.submit_transaction_with_outcome(transaction).unwrap(); 756 757 assert_eq!(outcome, TransactionSubmitOutcome::Added); 758 assert!(ledger.pending().is_empty()); 759 assert_eq!(ledger.orphan_transactions().len(), 1); 760 } 761 762 #[test] 763 fn vdf_retarget_observed_block_time_is_clamped() { 764 assert_eq!( 765 clamped_vdf_retarget_observed_block_ms(1), 766 MIN_VDF_RETARGET_OBSERVED_BLOCK_MS 767 ); 768 assert_eq!( 769 clamped_vdf_retarget_observed_block_ms(VDF_TARGET_BLOCK_MS), 770 VDF_TARGET_BLOCK_MS 771 ); 772 assert_eq!( 773 clamped_vdf_retarget_observed_block_ms(u64::MAX), 774 MAX_VDF_RETARGET_OBSERVED_BLOCK_MS 775 ); 776 } 777 778 #[test] 779 fn vdf_retarget_observed_block_time_ignores_ticket_fallback_ranks() { 780 let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); 781 let fallback_child = 782 vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 2, FinalizerMode::Ticket, 1); 783 784 assert_eq!( 785 vdf_retarget_observed_block_ms(&parent, &fallback_child), 786 None 787 ); 788 } 789 790 #[test] 791 fn vdf_retarget_observed_block_time_ignores_recovery_blocks() { 792 let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); 793 let recovery_child = 794 vdf_retarget_sample_block(RECOVERY_BLOCK_DELAY_MS, FinalizerMode::Recovery, 0); 795 796 assert_eq!( 797 vdf_retarget_observed_block_ms(&parent, &recovery_child), 798 None 799 ); 800 } 801 802 #[test] 803 fn vdf_retarget_keeps_rounds_inside_deadband() { 804 let current = 1_000; 805 let low_deadband_edge = 806 VDF_TARGET_BLOCK_MS - VDF_TARGET_BLOCK_MS * VDF_RETARGET_DEADBAND_PERCENT as u64 / 100; 807 let high_deadband_edge = 808 VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS * VDF_RETARGET_DEADBAND_PERCENT as u64 / 100; 809 810 assert_eq!(retarget_vdf_rounds(current, low_deadband_edge), current); 811 assert_eq!(retarget_vdf_rounds(current, VDF_TARGET_BLOCK_MS), current); 812 assert_eq!(retarget_vdf_rounds(current, high_deadband_edge), current); 813 } 814 815 #[test] 816 fn vdf_retarget_limits_each_step_to_two_percent() { 817 let current = 1_000; 818 819 assert_eq!( 820 retarget_vdf_rounds(current, MIN_VDF_RETARGET_OBSERVED_BLOCK_MS), 821 1_020 822 ); 823 assert_eq!( 824 retarget_vdf_rounds(current, MAX_VDF_RETARGET_OBSERVED_BLOCK_MS), 825 980 826 ); 827 } 828 829 #[test] 830 fn vdf_rounds_retarget_below_legacy_u32_limit_after_slow_blocks() { 831 let wallet = Wallet::from_seed("vdf-rounds-slow-above-u32"); 832 let initial_rounds = u64::from(u32::MAX); 833 let mut allocations = BTreeMap::new(); 834 allocations.insert(wallet.address().to_string(), 1_000); 835 let mut ledger = Ledger::new_with_genesis_burns( 836 allocations, 837 vec![GenesisBurn::new(wallet.address(), 1)], 838 initial_rounds, 839 ) 840 .unwrap(); 841 842 let block1 = apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); 843 assert_eq!(block1.vdf_rounds, initial_rounds); 844 assert_eq!(ledger.vdf_rounds(), initial_rounds); 845 846 let block2 = apply_preverified_burn_block_at( 847 &mut ledger, 848 &wallet, 849 VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS * 2, 850 ); 851 assert_eq!(block2.vdf_rounds, initial_rounds); 852 853 assert!( 854 ledger.vdf_rounds() < initial_rounds, 855 "slow blocks should retarget below the legacy u32 VDF rounds ceiling" 856 ); 857 } 858 859 #[test] 860 fn fallback_block_is_excluded_from_vdf_retarget_observations() { 861 let alice = Wallet::from_seed("fallback-retarget-alice"); 862 let bob = Wallet::from_seed("fallback-retarget-bob"); 863 let wallets = [&alice, &bob]; 864 let mut genesis = BTreeMap::new(); 865 genesis.insert(alice.address().to_string(), 1_000); 866 genesis.insert(bob.address().to_string(), 1_000); 867 let mut ledger = Ledger::new_with_genesis_burns( 868 genesis, 869 vec![ 870 GenesisBurn::new(alice.address(), 1), 871 GenesisBurn::new(bob.address(), 1), 872 ], 873 100, 874 ) 875 .unwrap(); 876 877 let primary = ledger.expected_leader_for_next_block().unwrap(); 878 let primary_wallet = wallets 879 .into_iter() 880 .find(|wallet| wallet.address() == primary) 881 .unwrap(); 882 apply_preverified_burn_block_at(&mut ledger, primary_wallet, VDF_TARGET_BLOCK_MS); 883 assert_eq!(ledger.vdf_rounds(), 100); 884 885 let primary = ledger.expected_leader_for_next_block().unwrap(); 886 let fallback = wallets 887 .into_iter() 888 .find(|wallet| wallet.address() != primary) 889 .unwrap(); 890 let timestamp_ms = ledger.tip().timestamp_ms + 1; 891 let block = apply_preverified_burn_block_at(&mut ledger, fallback, timestamp_ms); 892 893 assert_eq!(block.finalizer_rank, 1); 894 assert_eq!(block.vdf_rounds, 200); 895 assert_eq!(ledger.vdf_rounds(), 100); 896 } 897 898 #[test] 899 fn recovery_block_is_excluded_from_vdf_retarget_observations() { 900 let alice = Wallet::from_seed("recovery-retarget-alice"); 901 let bob = Wallet::from_seed("recovery-retarget-bob"); 902 let mut genesis = BTreeMap::new(); 903 genesis.insert(alice.address().to_string(), 1_000); 904 genesis.insert(bob.address().to_string(), 1_000); 905 let mut ledger = 906 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 100) 907 .unwrap(); 908 909 apply_preverified_burn_block_at(&mut ledger, &alice, VDF_TARGET_BLOCK_MS); 910 assert_eq!(ledger.vdf_rounds(), 100); 911 912 let burn = ledger.build_burn(&bob, 1, 0).unwrap(); 913 ledger.submit_transaction(burn).unwrap(); 914 let block = ledger 915 .mine_recovery_block(&bob, VDF_TARGET_BLOCK_MS + RECOVERY_BLOCK_DELAY_MS) 916 .unwrap(); 917 assert_eq!(block.finalizer_mode, FinalizerMode::Recovery); 918 ledger.apply_block(block).unwrap(); 919 920 assert_eq!(ledger.vdf_rounds(), 100); 921 } 922 923 #[test] 924 fn generated_vdf_retarget_decreases_after_slow_blocks_above_legacy_limit() { 925 let legacy_limit = u64::from(u32::MAX); 926 let slow_observed_ms = [ 927 VDF_TARGET_BLOCK_MS * 6 / 5, 928 VDF_TARGET_BLOCK_MS * 2, 929 VDF_TARGET_BLOCK_MS * 3, 930 MAX_VDF_RETARGET_OBSERVED_BLOCK_MS, 931 ]; 932 933 for seed in 0..16_u64 { 934 let wallet = Wallet::from_seed(&format!("generated-vdf-retarget-{seed}")); 935 let initial_rounds = legacy_limit + 1 + seed * 1_000_003; 936 let mut allocations = BTreeMap::new(); 937 allocations.insert(wallet.address().to_string(), 1_000); 938 let mut ledger = Ledger::new_with_genesis_burns( 939 allocations, 940 vec![GenesisBurn::new(wallet.address(), 1)], 941 initial_rounds, 942 ) 943 .unwrap(); 944 let observed_ms = slow_observed_ms[seed as usize % slow_observed_ms.len()]; 945 946 apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); 947 let second = apply_preverified_burn_block_at( 948 &mut ledger, 949 &wallet, 950 VDF_TARGET_BLOCK_MS + observed_ms, 951 ); 952 953 assert_eq!(second.vdf_rounds, initial_rounds); 954 assert!( 955 ledger.vdf_rounds() < initial_rounds, 956 "seed {seed} with observed {observed_ms}ms should lower VDF rounds from {initial_rounds}, got {}", 957 ledger.vdf_rounds() 958 ); 959 960 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 961 ledger.submit_transaction(burn).unwrap(); 962 let next_work = ledger 963 .prepare_next_block(wallet.address(), second.timestamp_ms + VDF_TARGET_BLOCK_MS) 964 .unwrap(); 965 assert_eq!(next_work.vdf_rounds(), ledger.vdf_rounds()); 966 } 967 } 968 969 #[test] 970 fn block_timestamp_future_check_uses_supplied_network_time() { 971 let wallet = Wallet::from_seed("adjusted-time-domain"); 972 let mut allocations = BTreeMap::new(); 973 allocations.insert(wallet.address().to_string(), 1_000); 974 let mut ledger = Ledger::new(allocations, 1); 975 976 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 977 assert!(ledger.submit_transaction(burn).unwrap()); 978 let block = ledger.mine_next_block(&wallet, 10 * 60 * 1_000).unwrap(); 979 980 let error = ledger.apply_block_at(block, 1_000).unwrap_err(); 981 982 assert!(format!("{error:#}").contains("too far in the future")); 983 } 984 985 #[test] 986 fn ticket_block_timestamp_uses_finalizer_rank_time_slot() { 987 let alice = Wallet::from_seed("rank-slot-alice"); 988 let bob = Wallet::from_seed("rank-slot-bob"); 989 let wallets = [alice.clone(), bob.clone()]; 990 let mut allocations = BTreeMap::new(); 991 allocations.insert(alice.address().to_string(), 1_000); 992 allocations.insert(bob.address().to_string(), 1_000); 993 let mut ledger = Ledger::new_with_genesis_burns( 994 allocations, 995 vec![ 996 GenesisBurn::new(alice.address(), 1), 997 GenesisBurn::new(bob.address(), 1), 998 ], 999 100, 1000 ) 1001 .unwrap(); 1002 1003 let primary = wallet_for_address(&wallets, &ledger.expected_leader_for_next_block().unwrap()); 1004 let burn = ledger.build_burn(primary, 1, 0).unwrap(); 1005 ledger.submit_transaction(burn).unwrap(); 1006 let work = ledger.prepare_next_block(primary.address(), 1).unwrap(); 1007 assert_eq!(work.timestamp_ms(), 1); 1008 let block = work.finish(primary, "preverified-vdf".to_string()); 1009 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 1010 1011 let fallback = wallets 1012 .iter() 1013 .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1)) 1014 .expect("expected rank 1 fallback"); 1015 let burn = ledger.build_burn(fallback, 1, 0).unwrap(); 1016 ledger.submit_transaction(burn).unwrap(); 1017 let parent_timestamp = ledger.tip().timestamp_ms; 1018 let work = ledger 1019 .prepare_next_block(fallback.address(), parent_timestamp + 1) 1020 .unwrap(); 1021 1022 assert_eq!( 1023 work.timestamp_ms(), 1024 parent_timestamp + VDF_TARGET_BLOCK_MS * 2 1025 ); 1026 assert_eq!(work.vdf_rounds(), ledger.vdf_rounds() * 2); 1027 } 1028 1029 #[test] 1030 fn required_anchor_burn_is_selected_before_higher_fee_burns_when_block_is_full() { 1031 let wallet = Wallet::from_seed("required-anchor-priority-wallet"); 1032 let mut allocations = BTreeMap::new(); 1033 allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA); 1034 let mut ledger = Ledger::new_with_genesis_burns( 1035 allocations, 1036 vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], 1037 1, 1038 ) 1039 .unwrap(); 1040 let high_fee_outpoint = named_test_outpoint("required-anchor-priority-high-fee"); 1041 let anchor_outpoint = named_test_outpoint("required-anchor-priority-anchor"); 1042 ledger.utxos.insert( 1043 high_fee_outpoint.clone(), 1044 TxOutput { 1045 address: wallet.address().to_string(), 1046 amount: 3, 1047 }, 1048 ); 1049 ledger.utxos.insert( 1050 anchor_outpoint.clone(), 1051 TxOutput { 1052 address: wallet.address().to_string(), 1053 amount: 2, 1054 }, 1055 ); 1056 let high_fee_burn = ledger 1057 .build_burn_with_inputs(&wallet, 1, 1, &[high_fee_outpoint]) 1058 .unwrap(); 1059 let anchor_burn = ledger 1060 .build_burn_with_inputs(&wallet, 1, 0, &[anchor_outpoint]) 1061 .unwrap(); 1062 ledger.submit_transaction(high_fee_burn).unwrap(); 1063 ledger.submit_transaction(anchor_burn.clone()).unwrap(); 1064 ledger.launch_profile.max_block_transactions = 1; 1065 1066 let work = ledger 1067 .prepare_next_block_with_required_burn_and_reveal_bundles( 1068 wallet.address(), 1069 1, 1070 Vec::new(), 1071 Some(anchor_burn.signature()), 1072 ) 1073 .unwrap(); 1074 let block = work.finish(&wallet, "preverified-vdf".to_string()); 1075 1076 assert_eq!(block.transactions.len(), 1); 1077 assert_eq!(block.transactions[0].signature(), anchor_burn.signature()); 1078 } 1079 1080 #[test] 1081 fn late_ticket_vdf_completion_is_visible_to_retarget() { 1082 let wallet = Wallet::from_seed("late-ticket-vdf-wallet"); 1083 let mut allocations = BTreeMap::new(); 1084 allocations.insert(wallet.address().to_string(), 1_000); 1085 let mut ledger = Ledger::new_with_genesis_burns( 1086 allocations, 1087 vec![GenesisBurn::new(wallet.address(), 1)], 1088 100, 1089 ) 1090 .unwrap(); 1091 1092 apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); 1093 assert_eq!(ledger.vdf_rounds(), 100); 1094 1095 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 1096 ledger.submit_transaction(burn).unwrap(); 1097 let work = ledger 1098 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 1099 .unwrap(); 1100 let scheduled_timestamp = work.timestamp_ms(); 1101 let late_timestamp = ledger.tip().timestamp_ms + VDF_TARGET_BLOCK_MS * 3; 1102 assert!(late_timestamp > scheduled_timestamp); 1103 1104 let block = work.finish_at(&wallet, "preverified-vdf".to_string(), late_timestamp); 1105 assert_eq!(block.timestamp_ms, late_timestamp); 1106 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 1107 1108 assert!( 1109 ledger.vdf_rounds() < 100, 1110 "late VDF completion should lower future VDF rounds" 1111 ); 1112 } 1113 1114 #[test] 1115 fn block_before_finalizer_rank_time_slot_is_rejected() { 1116 let alice = Wallet::from_seed("rank-slot-reject-alice"); 1117 let bob = Wallet::from_seed("rank-slot-reject-bob"); 1118 let wallets = [alice.clone(), bob.clone()]; 1119 let mut allocations = BTreeMap::new(); 1120 allocations.insert(alice.address().to_string(), 1_000); 1121 allocations.insert(bob.address().to_string(), 1_000); 1122 let mut ledger = Ledger::new_with_genesis_burns( 1123 allocations, 1124 vec![ 1125 GenesisBurn::new(alice.address(), 1), 1126 GenesisBurn::new(bob.address(), 1), 1127 ], 1128 100, 1129 ) 1130 .unwrap(); 1131 1132 let primary = wallet_for_address(&wallets, &ledger.expected_leader_for_next_block().unwrap()); 1133 let burn = ledger.build_burn(primary, 1, 0).unwrap(); 1134 ledger.submit_transaction(burn).unwrap(); 1135 let work = ledger.prepare_next_block(primary.address(), 1).unwrap(); 1136 let block = work.finish(primary, "preverified-vdf".to_string()); 1137 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 1138 1139 let fallback = wallets 1140 .iter() 1141 .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1)) 1142 .expect("expected rank 1 fallback"); 1143 let burn = ledger.build_burn(fallback, 1, 0).unwrap(); 1144 ledger.submit_transaction(burn).unwrap(); 1145 let parent_timestamp = ledger.tip().timestamp_ms; 1146 let work = ledger 1147 .prepare_next_block(fallback.address(), parent_timestamp + 1) 1148 .unwrap(); 1149 let mut block = work.finish(fallback, "preverified-vdf".to_string()); 1150 block.timestamp_ms = parent_timestamp + VDF_TARGET_BLOCK_MS * 2 - 1; 1151 block.hash = block.compute_hash(); 1152 1153 let error = ledger 1154 .apply_preverified_block_at(block, u64::MAX) 1155 .unwrap_err(); 1156 1157 assert!(format!("{error:#}").contains("before finalizer rank 1 time slot")); 1158 } 1159 1160 #[test] 1161 fn mempool_rejects_transaction_that_cannot_fit_in_a_block() { 1162 let alice = Wallet::from_seed("oversized-select-alice"); 1163 let bob = Wallet::from_seed("oversized-select-bob"); 1164 let mut allocations = BTreeMap::new(); 1165 allocations.insert(alice.address().to_string(), 1); 1166 allocations.insert(bob.address().to_string(), 300_000); 1167 let mut ledger = 1168 Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10) 1169 .unwrap(); 1170 let burn = ledger.build_burn(&alice, 1, 0).unwrap(); 1171 ledger.submit_transaction(burn).unwrap(); 1172 let oversized = 1173 transfer_with_extra_zero_outputs(&ledger, &bob, alice.address(), 1, 100_000, 4_000); 1174 assert!(oversized.serialized_size_bytes().unwrap() > MAX_BLOCK_BYTES); 1175 1176 let error = ledger.submit_transaction(oversized).unwrap_err(); 1177 1178 assert!(format!("{error:#}").contains("transaction exceeds max block size")); 1179 } 1180 1181 #[test] 1182 fn transfer_builder_rejects_selected_utxos_that_make_transaction_too_large() { 1183 let alice = Wallet::from_seed("oversized-selected-utxos-alice"); 1184 let bob = Wallet::from_seed("oversized-selected-utxos-bob"); 1185 let amounts = vec![1; 2_000]; 1186 let ledger = ledger_with_wallet_utxos(&alice, &amounts); 1187 let selected = (0..amounts.len()) 1188 .map(test_utxo_outpoint) 1189 .collect::<Vec<_>>(); 1190 1191 let error = ledger 1192 .build_transfer_with_inputs(&alice, bob.address(), 1, 0, &selected) 1193 .unwrap_err(); 1194 1195 assert!(format!("{error:#}").contains("transaction exceeds max block size")); 1196 } 1197 1198 #[test] 1199 fn transfer_can_spend_selected_utxos_when_they_cover_amount_and_fee() { 1200 let alice = Wallet::from_seed("selected-utxos-alice"); 1201 let bob = Wallet::from_seed("selected-utxos-bob"); 1202 let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); 1203 let selected = vec![test_utxo_outpoint(2)]; 1204 1205 let tx = ledger 1206 .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) 1207 .unwrap(); 1208 1209 let Transaction::Transfer { 1210 inputs, outputs, .. 1211 } = &tx 1212 else { 1213 panic!("expected transfer"); 1214 }; 1215 assert_eq!(inputs.len(), 1); 1216 assert_eq!(inputs[0].outpoint, selected[0]); 1217 assert_eq!( 1218 outputs, 1219 &[ 1220 TxOutput { 1221 address: bob.address().to_string(), 1222 amount: 2 1223 }, 1224 TxOutput { 1225 address: alice.address().to_string(), 1226 amount: 2 1227 } 1228 ] 1229 ); 1230 1231 ledger.submit_transaction(tx).unwrap(); 1232 let balances = pending_balances(&ledger); 1233 assert_eq!(balances.get(bob.address()).copied(), Some(2)); 1234 assert_eq!(balances.get(alice.address()).copied(), Some(7)); 1235 } 1236 1237 #[test] 1238 fn burn_can_spend_selected_utxos_when_they_cover_amount_and_fee() { 1239 let alice = Wallet::from_seed("selected-burn-utxos-alice"); 1240 let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); 1241 let selected = vec![test_utxo_outpoint(1)]; 1242 1243 let tx = ledger 1244 .build_burn_with_inputs(&alice, 1, 1, &selected) 1245 .unwrap(); 1246 1247 let Transaction::Burn { 1248 inputs, 1249 change, 1250 amount, 1251 fee, 1252 .. 1253 } = &tx 1254 else { 1255 panic!("expected burn"); 1256 }; 1257 assert_eq!(*amount, 1); 1258 assert_eq!(*fee, 1); 1259 assert_eq!(inputs.len(), 1); 1260 assert_eq!(inputs[0].outpoint, selected[0]); 1261 assert_eq!( 1262 change, 1263 &[TxOutput { 1264 address: alice.address().to_string(), 1265 amount: 1 1266 }] 1267 ); 1268 1269 ledger.submit_transaction(tx).unwrap(); 1270 let balances = pending_balances(&ledger); 1271 assert_eq!(balances.get(alice.address()).copied(), Some(8)); 1272 } 1273 1274 #[test] 1275 fn transfer_rejects_selected_utxos_that_do_not_cover_amount_plus_fee() { 1276 let alice = Wallet::from_seed("selected-utxos-insufficient-alice"); 1277 let bob = Wallet::from_seed("selected-utxos-insufficient-bob"); 1278 let ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); 1279 let selected = vec![test_utxo_outpoint(0)]; 1280 1281 let error = ledger 1282 .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) 1283 .unwrap_err(); 1284 1285 assert!(format!("{error:#}").contains("selected UTXOs do not cover")); 1286 } 1287 1288 #[test] 1289 fn transfer_rejects_selected_utxos_owned_by_someone_else() { 1290 let alice = Wallet::from_seed("selected-utxos-owner-alice"); 1291 let bob = Wallet::from_seed("selected-utxos-owner-bob"); 1292 let carol = Wallet::from_seed("selected-utxos-owner-carol"); 1293 let mut ledger = ledger_with_wallet_utxos(&alice, &[5]); 1294 ledger.utxos.insert( 1295 named_test_outpoint("carol"), 1296 TxOutput { 1297 address: carol.address().to_string(), 1298 amount: 5, 1299 }, 1300 ); 1301 let selected = vec![named_test_outpoint("carol")]; 1302 1303 let error = ledger 1304 .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) 1305 .unwrap_err(); 1306 1307 assert!(format!("{error:#}").contains("is not owned")); 1308 } 1309 1310 #[test] 1311 fn transfer_rejects_when_combined_utxos_do_not_cover_amount_plus_fee() { 1312 let alice = Wallet::from_seed("combine-insufficient-alice"); 1313 let bob = Wallet::from_seed("combine-insufficient-bob"); 1314 let ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 1]); 1315 1316 let error = ledger 1317 .build_transfer(&alice, bob.address(), 3, 1) 1318 .unwrap_err(); 1319 1320 assert!(format!("{error:#}").contains("insufficient funds")); 1321 } 1322 1323 #[test] 1324 fn pending_change_from_combined_utxos_can_fund_next_transaction() { 1325 let alice = Wallet::from_seed("combine-pending-change-alice"); 1326 let bob = Wallet::from_seed("combine-pending-change-bob"); 1327 let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); 1328 1329 let first = ledger.build_transfer(&alice, bob.address(), 3, 0).unwrap(); 1330 let first_signature = first.signature().to_string(); 1331 ledger.submit_transaction(first).unwrap(); 1332 1333 let second = ledger.build_transfer(&alice, bob.address(), 1, 0).unwrap(); 1334 let Transaction::Transfer { inputs, .. } = &second else { 1335 panic!("expected transfer"); 1336 }; 1337 assert_eq!(inputs.len(), 1); 1338 assert_eq!(inputs[0].outpoint.txid, first_signature); 1339 assert_eq!(inputs[0].outpoint.index, 1); 1340 1341 ledger.submit_transaction(second).unwrap(); 1342 let balances = pending_balances(&ledger); 1343 assert_eq!( 1344 balances.get(alice.address()).copied().unwrap_or_default(), 1345 0 1346 ); 1347 assert_eq!(balances.get(bob.address()).copied(), Some(4)); 1348 } 1349 1350 #[test] 1351 fn winning_burn_ticket_is_consumed_even_when_window_remains() { 1352 let parent = ticket_test_block(3, FinalizerMode::Ticket, 0, "parent"); 1353 let mut tickets = vec![ 1354 BurnTicket { 1355 id: "high-burn".to_string(), 1356 owner: "alice".to_string(), 1357 amount: 10_000, 1358 eligible_from_height: 4, 1359 eligible_until_height: 6, 1360 }, 1361 BurnTicket { 1362 id: "small-burn".to_string(), 1363 owner: "bob".to_string(), 1364 amount: 1, 1365 eligible_from_height: 5, 1366 eligible_until_height: 7, 1367 }, 1368 ]; 1369 let block = ticket_test_child(&parent, 4, "alice", 0, "high-burn"); 1370 1371 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1372 1373 assert!( 1374 tickets.iter().all(|ticket| ticket.id != "high-burn"), 1375 "a winning burn must not remain eligible for the rest of its window" 1376 ); 1377 assert!( 1378 tickets.iter().any(|ticket| ticket.id == "small-burn"), 1379 "unselected future tickets should remain pending" 1380 ); 1381 } 1382 1383 fn ticket_test_block( 1384 height: u64, 1385 finalizer_mode: FinalizerMode, 1386 finalizer_rank: u32, 1387 seed: &str, 1388 ) -> Block { 1389 let mut block = Block { 1390 height, 1391 prev_hash: hex_hash(format!("ticket-test-prev:{seed}:{height}")), 1392 timestamp_ms: height, 1393 miner: seed.to_string(), 1394 finalizer_mode, 1395 finalizer_rank, 1396 reward: BLOCK_REWARD, 1397 vdf_rounds: 1, 1398 vdf_output: hex_hash(format!("ticket-test-vdf:{seed}:{height}")), 1399 leader_proof: None, 1400 blinded_transactions: Vec::new(), 1401 reveal_bundle_section: RevealBundleSection::default(), 1402 transactions: Vec::new(), 1403 hash: String::new(), 1404 }; 1405 block.hash = block.compute_hash(); 1406 block 1407 } 1408 1409 fn ticket_test_child( 1410 parent: &Block, 1411 height: u64, 1412 miner: &str, 1413 finalizer_rank: u32, 1414 ticket_id: &str, 1415 ) -> Block { 1416 let mut block = ticket_test_block(height, FinalizerMode::Ticket, finalizer_rank, miner); 1417 block.prev_hash = parent.hash.clone(); 1418 block.leader_proof = Some(LeaderProof { 1419 ticket_id: ticket_id.to_string(), 1420 public_key: miner.to_string(), 1421 signature: "signature".to_string(), 1422 }); 1423 block.hash = block.compute_hash(); 1424 block 1425 } 1426 1427 fn fallback_invalidation_tickets(height: u64) -> Vec<BurnTicket> { 1428 vec![ 1429 BurnTicket { 1430 id: "alice-main".to_string(), 1431 owner: "alice".to_string(), 1432 amount: 10, 1433 eligible_from_height: height, 1434 eligible_until_height: height + 2, 1435 }, 1436 BurnTicket { 1437 id: "alice-other-eligible".to_string(), 1438 owner: "alice".to_string(), 1439 amount: 7, 1440 eligible_from_height: height, 1441 eligible_until_height: height + 2, 1442 }, 1443 BurnTicket { 1444 id: "bob-main".to_string(), 1445 owner: "bob".to_string(), 1446 amount: 9, 1447 eligible_from_height: height, 1448 eligible_until_height: height + 2, 1449 }, 1450 BurnTicket { 1451 id: "bob-other-eligible".to_string(), 1452 owner: "bob".to_string(), 1453 amount: 6, 1454 eligible_from_height: height, 1455 eligible_until_height: height + 2, 1456 }, 1457 BurnTicket { 1458 id: "carol-main".to_string(), 1459 owner: "carol".to_string(), 1460 amount: 8, 1461 eligible_from_height: height, 1462 eligible_until_height: height + 2, 1463 }, 1464 BurnTicket { 1465 id: "future-same-owner".to_string(), 1466 owner: "alice".to_string(), 1467 amount: 5, 1468 eligible_from_height: height + 1, 1469 eligible_until_height: height + 3, 1470 }, 1471 ] 1472 } 1473 1474 #[test] 1475 fn primary_ticket_at_activation_height_only_consumes_winning_ticket() { 1476 let height = MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT; 1477 let parent = ticket_test_block(height - 1, FinalizerMode::Ticket, 0, "primary-parent"); 1478 let mut tickets = fallback_invalidation_tickets(height); 1479 let ranked = ranked_tickets_for_height(&parent, height, &tickets); 1480 let leader = &ranked[0]; 1481 let leader_id = leader.id.clone(); 1482 let leader_owner = leader.owner.clone(); 1483 let eligible_before = tickets 1484 .iter() 1485 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1486 .count(); 1487 let block = ticket_test_child(&parent, height, &leader_owner, 0, &leader_id); 1488 1489 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1490 1491 assert!(tickets.iter().all(|ticket| ticket.id != leader_id)); 1492 let eligible_after = tickets 1493 .iter() 1494 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1495 .count(); 1496 assert_eq!(eligible_after, eligible_before - 1); 1497 } 1498 1499 #[test] 1500 fn fallback_ticket_before_activation_height_only_consumes_winning_ticket() { 1501 let height = MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT - 1; 1502 let parent = ticket_test_block( 1503 height - 1, 1504 FinalizerMode::Ticket, 1505 0, 1506 "legacy-fallback-parent", 1507 ); 1508 let mut tickets = fallback_invalidation_tickets(height); 1509 let ranked = ranked_tickets_for_height(&parent, height, &tickets); 1510 let fallback = &ranked[1]; 1511 let fallback_id = fallback.id.clone(); 1512 let fallback_owner = fallback.owner.clone(); 1513 let eligible_before = tickets 1514 .iter() 1515 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1516 .count(); 1517 let block = ticket_test_child(&parent, height, &fallback_owner, 1, &fallback_id); 1518 1519 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1520 1521 assert!(tickets.iter().all(|ticket| ticket.id != fallback_id)); 1522 let eligible_after = tickets 1523 .iter() 1524 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1525 .count(); 1526 assert_eq!(eligible_after, eligible_before - 1); 1527 } 1528 1529 #[test] 1530 fn fallback_ticket_invalidates_missed_ranks_and_current_sibling_tickets_from_height_300() { 1531 let height = MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT; 1532 let parent = ticket_test_block(height - 1, FinalizerMode::Ticket, 0, "fallback-parent"); 1533 let mut tickets = fallback_invalidation_tickets(height); 1534 let ranked = ranked_tickets_for_height(&parent, height, &tickets); 1535 let prefix_owners = ranked 1536 .iter() 1537 .take(2) 1538 .map(|ticket| ticket.owner.clone()) 1539 .collect::<BTreeSet<_>>(); 1540 let fallback = &ranked[1]; 1541 let block = ticket_test_child(&parent, height, &fallback.owner, 1, &fallback.id); 1542 1543 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1544 1545 assert!( 1546 tickets.iter().all(|ticket| { 1547 !ticket_is_eligible_for_height(ticket, height) || !prefix_owners.contains(&ticket.owner) 1548 }), 1549 "eligible tickets from missed ranks and the fallback finalizer should be invalidated" 1550 ); 1551 assert!( 1552 tickets 1553 .iter() 1554 .any(|ticket| ticket.id == "future-same-owner"), 1555 "future tickets from invalidated owners should stay pending" 1556 ); 1557 } 1558 1559 #[test] 1560 fn burn_leader_ranks_for_block_reconstructs_historical_ticket_order() { 1561 let alice = Wallet::from_seed("burn-rank-alice"); 1562 let bob = Wallet::from_seed("burn-rank-bob"); 1563 let mut allocations = BTreeMap::new(); 1564 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 1565 allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); 1566 let ledger = Ledger::new_with_genesis_burns( 1567 allocations, 1568 vec![ 1569 GenesisBurn::new(alice.address(), MICRO_IUNA), 1570 GenesisBurn::new(bob.address(), MICRO_IUNA), 1571 ], 1572 1, 1573 ) 1574 .unwrap(); 1575 1576 let ranks = ledger.burn_leader_ranks_for_block(1).unwrap(); 1577 let leader = ledger.expected_leader_for_next_block().unwrap(); 1578 1579 assert_eq!(ranks.len(), 2); 1580 assert_eq!(ranks[0].rank, 0); 1581 assert_eq!(ranks[0].owner, leader); 1582 assert!(ranks.iter().all(|rank| rank.amount == MICRO_IUNA)); 1583 assert_eq!(ledger.burn_leader_ranks_for_block(0).unwrap(), Vec::new()); 1584 1585 let batch = ledger.burn_leader_ranks_for_blocks([0, 1]).unwrap(); 1586 assert!(batch.get(&0).unwrap().is_empty()); 1587 assert_eq!(batch.get(&1), Some(&ranks)); 1588 } 1589 1590 #[test] 1591 fn mine_recipient_is_bound_to_proof_hash() { 1592 let alice = Wallet::from_seed("mine-proof-alice"); 1593 let bob = Wallet::from_seed("mine-proof-bob"); 1594 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1595 let mut forged = unsigned_mine(&ledger, alice.address()); 1596 if let Transaction::Mine { recipient, .. } = &mut forged { 1597 *recipient = bob.address().to_string(); 1598 } 1599 1600 let error = ledger.submit_transaction(forged).unwrap_err(); 1601 1602 assert!(format!("{error:#}").contains("proof hash is invalid")); 1603 } 1604 1605 #[test] 1606 fn mine_action_uses_fixed_reward_and_fixed_finalizer_fee() { 1607 let alice = Wallet::from_seed("mine-fixed-reward-alice"); 1608 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1609 1610 let mine = ledger.build_mine(alice.address()).unwrap(); 1611 1612 assert_eq!(mine.amount(), MINE_REWARD); 1613 assert_eq!(mine.fee(), MINE_FINALIZER_FEE); 1614 assert!(ledger.submit_transaction(mine).unwrap()); 1615 } 1616 1617 #[test] 1618 fn burn_fee_goes_to_block_finalizer() { 1619 let alice = Wallet::from_seed("burn-fee-finalizer-alice"); 1620 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1621 1622 let burn_fee = 12; 1623 let burn = ledger 1624 .build_burn(&alice, TEST_BURN_AMOUNT, burn_fee) 1625 .unwrap(); 1626 ledger.submit_transaction(burn).unwrap(); 1627 let prepared = ledger.prepare_next_block(alice.address(), 1).unwrap(); 1628 1629 assert_eq!(prepared.reward, burn_fee); 1630 } 1631 1632 #[test] 1633 fn blinded_burn_commits_ciphertext_and_reveal_executes_later() { 1634 let alice = Wallet::from_seed("blinded-burn-finalizer-alice"); 1635 let bob = Wallet::from_seed("blinded-burn-finalizer-bob"); 1636 let carol = Wallet::from_seed("blinded-burn-carol"); 1637 let finalizers = [alice.clone(), bob.clone()]; 1638 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1639 let fee = 100; 1640 let burn_amount = 3; 1641 let before_carol = ledger.balance_of(carol.address()); 1642 1643 let blinded = ledger 1644 .build_blinded_burn(&carol, burn_amount, fee, ledger.height() + 4) 1645 .unwrap(); 1646 assert!(!blinded.transaction.ciphertext.contains("burn")); 1647 assert!(!blinded.transaction.ciphertext.contains(carol.address())); 1648 ledger 1649 .submit_blinded_transaction(blinded.transaction.clone()) 1650 .unwrap(); 1651 queue_next_leader_burn(&mut ledger, &finalizers); 1652 1653 let commit_block = mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1654 let inclusion_finalizer = commit_block.miner.clone(); 1655 assert_eq!( 1656 commit_block 1657 .transactions 1658 .iter() 1659 .filter(|transaction| transaction.is_burn()) 1660 .count(), 1661 1 1662 ); 1663 assert_eq!(commit_block.blinded_transactions, vec![blinded.transaction]); 1664 assert_eq!(commit_block.reward, 0); 1665 let before_inclusion_finalizer = ledger.balance_of(&inclusion_finalizer); 1666 1667 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1668 queue_next_leader_burn(&mut ledger, &finalizers); 1669 let reveal_block = 1670 mine_preverified_as_next_leader_with_reveal_bundles(&mut ledger, &finalizers, 2); 1671 let reveal_executor = reveal_block.miner.clone(); 1672 1673 assert_eq!(reveal_block.all_blinded_reveals().len(), 1); 1674 assert_eq!( 1675 ledger.balance_of(carol.address()), 1676 before_carol - burn_amount - fee 1677 ); 1678 assert!(ledger.tickets.iter().any(|ticket| { 1679 ticket.owner == carol.address() 1680 && ticket.amount == burn_amount 1681 && ticket.eligible_from_height 1682 == reveal_block.height + ledger.launch_profile.ticket_maturity_delay_heights 1683 })); 1684 let reveal_plaintext_burn_spent_by_inclusion_finalizer = reveal_block 1685 .transactions 1686 .iter() 1687 .filter(|transaction| { 1688 transaction.is_burn() && transaction.sender() == inclusion_finalizer.as_str() 1689 }) 1690 .fold(0_u64, |total, transaction| { 1691 total + transaction.amount() + transaction.fee() 1692 }); 1693 let committer_fee = blinded_fee_share(fee, BLINDED_COMMITTER_FEE_BPS); 1694 let reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1695 fee, 1696 reveal_block.included_reveal_bundle_count(), 1697 ledger 1698 .burn_leader_ranks_for_block(reveal_block.height) 1699 .unwrap() 1700 .len(), 1701 ); 1702 let reveal_bundle_signer_fee = blinded_fee_share(fee, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS); 1703 let commitment = &commit_block.blinded_transactions[0].commitment; 1704 assert_eq!( 1705 ledger 1706 .utxos 1707 .get(&blinded_committer_fee_outpoint(commitment)) 1708 .unwrap(), 1709 &TxOutput { 1710 address: inclusion_finalizer.clone(), 1711 amount: committer_fee, 1712 } 1713 ); 1714 assert!( 1715 !ledger 1716 .utxos 1717 .contains_key(&blinded_executor_fee_outpoint(commitment)) 1718 ); 1719 assert_eq!(reveal_block.reward, reveal_finalizer_fee); 1720 assert_eq!( 1721 ledger 1722 .utxos 1723 .get(&reward_outpoint(&reveal_block.hash)) 1724 .unwrap(), 1725 &TxOutput { 1726 address: reveal_executor.clone(), 1727 amount: reveal_finalizer_fee, 1728 } 1729 ); 1730 for signature in &reveal_block.reveal_bundle_section.signatures { 1731 assert_eq!( 1732 ledger 1733 .utxos 1734 .get(&blinded_reveal_bundle_signer_fee_outpoint( 1735 commitment, 1736 signature.slot 1737 )) 1738 .unwrap(), 1739 &TxOutput { 1740 address: signature.member.clone(), 1741 amount: reveal_bundle_signer_fee, 1742 } 1743 ); 1744 } 1745 let mut inclusion_finalizer_fee = committer_fee; 1746 if inclusion_finalizer == reveal_executor { 1747 inclusion_finalizer_fee += reveal_finalizer_fee; 1748 } 1749 inclusion_finalizer_fee += reveal_block 1750 .reveal_bundle_section 1751 .signatures 1752 .iter() 1753 .filter(|signature| signature.member == inclusion_finalizer) 1754 .count() as u64 1755 * reveal_bundle_signer_fee; 1756 assert_eq!( 1757 ledger.balance_of(&inclusion_finalizer), 1758 before_inclusion_finalizer + inclusion_finalizer_fee 1759 - reveal_plaintext_burn_spent_by_inclusion_finalizer 1760 ); 1761 } 1762 1763 #[test] 1764 fn blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { 1765 let alice = Wallet::from_seed("aggregated-finalizer-fee-alice"); 1766 let bob = Wallet::from_seed("aggregated-finalizer-fee-bob"); 1767 let carol = Wallet::from_seed("aggregated-finalizer-fee-carol"); 1768 let dave = Wallet::from_seed("aggregated-finalizer-fee-dave"); 1769 let finalizers = [alice.clone(), bob.clone()]; 1770 let mut ledger = ledger_with_finalizers( 1771 &finalizers, 1772 &[(&carol, 10 * MICRO_IUNA), (&dave, 10 * MICRO_IUNA)], 1773 ); 1774 let first_fee = 100; 1775 let second_fee = 200; 1776 let first_blinded = ledger 1777 .build_blinded_burn(&carol, 3, first_fee, ledger.height() + 4) 1778 .unwrap(); 1779 let second_blinded = ledger 1780 .build_blinded_burn(&dave, 4, second_fee, ledger.height() + 4) 1781 .unwrap(); 1782 let first_commitment = first_blinded.transaction.commitment.clone(); 1783 let second_commitment = second_blinded.transaction.commitment.clone(); 1784 ledger 1785 .submit_blinded_transaction(first_blinded.transaction) 1786 .unwrap(); 1787 ledger 1788 .submit_blinded_transaction(second_blinded.transaction) 1789 .unwrap(); 1790 queue_next_leader_burn(&mut ledger, &finalizers); 1791 let commit_timestamp_ms = ledger 1792 .tip() 1793 .timestamp_ms 1794 .saturating_add(VDF_TARGET_BLOCK_MS); 1795 let commit_block = 1796 mine_preverified_as_next_leader(&mut ledger, &finalizers, commit_timestamp_ms); 1797 assert_eq!(commit_block.height, 1); 1798 1799 ledger.submit_blinded_reveal(first_blinded.reveal).unwrap(); 1800 ledger.submit_blinded_reveal(second_blinded.reveal).unwrap(); 1801 queue_next_leader_burn(&mut ledger, &finalizers); 1802 let reveal_timestamp_ms = ledger 1803 .tip() 1804 .timestamp_ms 1805 .saturating_add(VDF_TARGET_BLOCK_MS); 1806 let reveal_block = prepare_preverified_as_next_leader_with_reveal_bundles( 1807 &ledger, 1808 &finalizers, 1809 reveal_timestamp_ms, 1810 ); 1811 let first_reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1812 first_fee, 1813 reveal_block.included_reveal_bundle_count(), 1814 ledger 1815 .burn_leader_ranks_for_block(reveal_block.height) 1816 .unwrap() 1817 .len(), 1818 ); 1819 let second_reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1820 second_fee, 1821 reveal_block.included_reveal_bundle_count(), 1822 ledger 1823 .burn_leader_ranks_for_block(reveal_block.height) 1824 .unwrap() 1825 .len(), 1826 ); 1827 let aggregate_reveal_finalizer_fee = first_reveal_finalizer_fee 1828 .checked_add(second_reveal_finalizer_fee) 1829 .unwrap(); 1830 1831 assert_eq!(reveal_block.height, 2); 1832 assert_eq!(reveal_block.reward, aggregate_reveal_finalizer_fee); 1833 let mut plain_fee_reward_block = reveal_block.clone(); 1834 plain_fee_reward_block.reward = fee_reward(&plain_fee_reward_block.transactions).unwrap(); 1835 plain_fee_reward_block.hash = plain_fee_reward_block.compute_hash(); 1836 let error = ledger 1837 .clone() 1838 .apply_preverified_block_at(plain_fee_reward_block, u64::MAX) 1839 .unwrap_err(); 1840 assert!(format!("{error:#}").contains("block reward is invalid")); 1841 1842 ledger 1843 .apply_preverified_block_at(reveal_block.clone(), u64::MAX) 1844 .unwrap(); 1845 assert!( 1846 !ledger 1847 .utxos 1848 .contains_key(&blinded_executor_fee_outpoint(&first_commitment)) 1849 ); 1850 assert!( 1851 !ledger 1852 .utxos 1853 .contains_key(&blinded_executor_fee_outpoint(&second_commitment)) 1854 ); 1855 assert_eq!( 1856 ledger.utxos.get(&reward_outpoint(&reveal_block.hash)), 1857 Some(&TxOutput { 1858 address: reveal_block.miner.clone(), 1859 amount: aggregate_reveal_finalizer_fee, 1860 }) 1861 ); 1862 } 1863 1864 #[test] 1865 fn blinded_utxo_commit_exposes_and_locks_inputs_until_reveal_or_expiry() { 1866 let alice = Wallet::from_seed("blinded-lock-alice"); 1867 let bob = Wallet::from_seed("blinded-lock-bob"); 1868 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 1869 let transfer = ledger.build_transfer(&alice, bob.address(), 3, 2).unwrap(); 1870 let visible_inputs = transfer.inputs().to_vec(); 1871 1872 let blinded = ledger 1873 .build_blinded_transaction(&alice, transfer, ledger.height() + 4) 1874 .unwrap(); 1875 1876 assert_eq!(blinded.transaction.inputs.len(), visible_inputs.len()); 1877 assert_eq!( 1878 unsigned_inputs(&blinded.transaction.inputs), 1879 unsigned_inputs(&visible_inputs) 1880 ); 1881 ledger 1882 .submit_blinded_transaction(blinded.transaction) 1883 .unwrap(); 1884 let error = ledger 1885 .build_transfer(&alice, bob.address(), 1, 0) 1886 .unwrap_err(); 1887 assert!(format!("{error:#}").contains("insufficient funds")); 1888 } 1889 1890 #[test] 1891 fn blinded_payload_omits_visible_inputs_and_reconstructs_transaction_on_reveal() { 1892 let alice = Wallet::from_seed("blinded-compact-payload-alice"); 1893 let bob = Wallet::from_seed("blinded-compact-payload-bob"); 1894 let ledger = ledger_with_wallet_utxos(&alice, &[10]); 1895 let transfer = ledger.build_transfer(&alice, bob.address(), 3, 2).unwrap(); 1896 let full_transaction_bytes = serde_json::to_vec(&transfer).unwrap().len(); 1897 let blinded = ledger 1898 .build_blinded_transaction(&alice, transfer.clone(), ledger.height() + 4) 1899 .unwrap(); 1900 let key = decode_hex_array::<BLINDED_KEY_BYTES>(&blinded.reveal.key).unwrap(); 1901 let nonce = decode_hex_array::<BLINDED_NONCE_BYTES>(&blinded.transaction.nonce).unwrap(); 1902 let ciphertext = decode_hex(&blinded.transaction.ciphertext).unwrap(); 1903 1904 let plaintext = decrypt_blinded_payload( 1905 &key, 1906 &nonce, 1907 &signed_blinded_inputs(&unsigned_inputs(&blinded.transaction.inputs), ""), 1908 blinded.transaction.fee, 1909 blinded.transaction.expires_at_height, 1910 &ciphertext, 1911 ) 1912 .unwrap(); 1913 let payload: serde_json::Value = serde_json::from_slice(&plaintext).unwrap(); 1914 let revealed = decrypt_blinded_transaction(&blinded.transaction, &blinded.reveal).unwrap(); 1915 1916 assert_eq!( 1917 payload.get("kind").and_then(|kind| kind.as_str()), 1918 Some("transfer") 1919 ); 1920 assert!(payload.get("inputs").is_none()); 1921 assert!(plaintext.len() < full_transaction_bytes); 1922 assert_eq!(revealed, transfer); 1923 } 1924 1925 #[test] 1926 fn unrevealed_blinded_utxo_commit_burns_fee_and_returns_change() { 1927 let alice = Wallet::from_seed("blinded-expiry-alice"); 1928 let bob = Wallet::from_seed("blinded-expiry-bob"); 1929 let carol = Wallet::from_seed("blinded-expiry-carol"); 1930 let finalizers = [alice.clone(), bob.clone()]; 1931 let carol_balance = 10 * MICRO_IUNA; 1932 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, carol_balance)]); 1933 let fee = 100; 1934 let blinded = ledger 1935 .build_blinded_burn(&carol, 3, fee, ledger.height() + 2) 1936 .unwrap(); 1937 let commitment = blinded.transaction.commitment.clone(); 1938 ledger 1939 .submit_blinded_transaction(blinded.transaction) 1940 .unwrap(); 1941 queue_next_leader_burn(&mut ledger, &finalizers); 1942 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1943 1944 assert_eq!(ledger.balance_of(carol.address()), 0); 1945 queue_next_leader_burn(&mut ledger, &finalizers); 1946 mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); 1947 1948 assert_eq!( 1949 ledger 1950 .utxos 1951 .get(&blinded_committer_fee_outpoint(&commitment)), 1952 None 1953 ); 1954 assert_eq!( 1955 ledger 1956 .utxos 1957 .get(&blinded_expiry_change_outpoint(&commitment)), 1958 Some(&TxOutput { 1959 address: carol.address().to_string(), 1960 amount: carol_balance - fee, 1961 }) 1962 ); 1963 assert_eq!(ledger.balance_of(carol.address()), carol_balance - fee); 1964 } 1965 1966 #[test] 1967 fn fee_bearing_blinded_commit_without_inputs_is_rejected() { 1968 let alice = Wallet::from_seed("blinded-no-input-fee-alice"); 1969 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1970 let mut blinded = ledger 1971 .build_blinded_burn(&alice, 1, 1, ledger.height() + 4) 1972 .unwrap() 1973 .transaction; 1974 blinded.inputs.clear(); 1975 blinded.commitment = blinded_transaction_commitment(&blinded).unwrap(); 1976 1977 let error = ledger.submit_blinded_transaction(blinded).unwrap_err(); 1978 1979 assert!(format!("{error:#}").contains("must lock visible inputs")); 1980 } 1981 1982 #[test] 1983 fn mine_actions_cannot_be_blinded() { 1984 let alice = Wallet::from_seed("blinded-mine-collateral-alice"); 1985 let ledger = ledger_with_finalizers(&[alice.clone()], &[]); 1986 let mine = ledger.build_mine(alice.address()).unwrap(); 1987 let error = ledger 1988 .build_blinded_transaction(&alice, mine, ledger.height() + 4) 1989 .unwrap_err(); 1990 1991 assert!(format!("{error:#}").contains("mine actions are public")); 1992 } 1993 1994 #[test] 1995 fn reveal_bundle_hashes_are_bound_to_next_block_vdf_seed() { 1996 let alice = Wallet::from_seed("bundle-seed-alice"); 1997 let bob = Wallet::from_seed("bundle-seed-bob"); 1998 let carol = Wallet::from_seed("bundle-seed-carol"); 1999 let finalizers = [alice.clone(), bob.clone()]; 2000 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2001 let blinded = ledger 2002 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2003 .unwrap(); 2004 ledger 2005 .submit_blinded_transaction(blinded.transaction.clone()) 2006 .unwrap(); 2007 queue_next_leader_burn(&mut ledger, &finalizers); 2008 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2009 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2010 queue_next_leader_burn(&mut ledger, &finalizers); 2011 let leader = ledger.expected_leader_for_next_block().unwrap(); 2012 let leader_wallet = wallet_for_address(&finalizers, &leader); 2013 let bundles = ledger 2014 .reveal_committee_for_next_block() 2015 .into_iter() 2016 .filter_map(|member| { 2017 let wallet = wallet_for_address(&finalizers, &member.owner); 2018 ledger.build_reveal_bundle(wallet).unwrap() 2019 }) 2020 .collect::<Vec<_>>(); 2021 if bundles.len() > 1 { 2022 let mut reversed = bundles.clone(); 2023 reversed.reverse(); 2024 let error = ledger 2025 .validate_next_block_reveal_bundles(reversed) 2026 .unwrap_err(); 2027 assert!(format!("{error:#}").contains("reveal bundles are not in slot order")); 2028 } 2029 2030 let without_bundles = ledger 2031 .prepare_next_block(leader_wallet.address(), ledger.tip().timestamp_ms + 1) 2032 .unwrap(); 2033 let with_bundles = ledger 2034 .prepare_next_block_with_reveal_bundles( 2035 leader_wallet.address(), 2036 ledger.tip().timestamp_ms + 1, 2037 bundles, 2038 ) 2039 .unwrap(); 2040 2041 assert_ne!(without_bundles.vdf_seed(), with_bundles.vdf_seed()); 2042 } 2043 2044 #[test] 2045 fn reveal_committee_includes_next_block_finalizer_as_slot_zero() { 2046 let alice = Wallet::from_seed("bundle-finalizer-slot-alice"); 2047 let bob = Wallet::from_seed("bundle-finalizer-slot-bob"); 2048 let carol = Wallet::from_seed("bundle-finalizer-slot-carol"); 2049 let dave = Wallet::from_seed("bundle-finalizer-slot-dave"); 2050 let erin = Wallet::from_seed("bundle-finalizer-slot-erin"); 2051 let finalizers = [alice.clone(), bob.clone(), carol.clone(), dave.clone()]; 2052 let mut ledger = ledger_with_finalizers(&finalizers, &[(&erin, 10 * MICRO_IUNA)]); 2053 let blinded = ledger 2054 .build_blinded_burn(&erin, 3, 7, ledger.height() + 4) 2055 .unwrap(); 2056 let commitment = blinded.transaction.commitment.clone(); 2057 ledger 2058 .submit_blinded_transaction(blinded.transaction) 2059 .unwrap(); 2060 queue_next_leader_burn(&mut ledger, &finalizers); 2061 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2062 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2063 queue_next_leader_burn(&mut ledger, &finalizers); 2064 2065 let leader = ledger.expected_leader_for_next_block().unwrap(); 2066 let committee = ledger.reveal_committee_for_next_block(); 2067 let leader_wallet = wallet_for_address(&finalizers, &leader); 2068 let bundle = ledger.build_reveal_bundle(leader_wallet).unwrap().unwrap(); 2069 2070 assert_eq!(committee.first().map(|member| member.slot), Some(0)); 2071 assert_eq!(committee.first().map(|member| member.rank), Some(0)); 2072 assert_eq!( 2073 committee.first().map(|member| member.owner.as_str()), 2074 Some(leader.as_str()) 2075 ); 2076 assert_eq!(bundle.slot, 0); 2077 assert_eq!(bundle.member, leader); 2078 assert!( 2079 bundle 2080 .reveals 2081 .iter() 2082 .any(|reveal| reveal.commitment == commitment) 2083 ); 2084 } 2085 2086 #[test] 2087 fn reveal_bundle_section_deduplicates_reveals_with_slot_mask() { 2088 let alice = Wallet::from_seed("bundle-compact-alice"); 2089 let bob = Wallet::from_seed("bundle-compact-bob"); 2090 let carol = Wallet::from_seed("bundle-compact-carol"); 2091 let dave = Wallet::from_seed("bundle-compact-dave"); 2092 let finalizers = [alice.clone(), bob.clone(), carol.clone()]; 2093 let mut ledger = ledger_with_finalizers(&finalizers, &[(&dave, 10 * MICRO_IUNA)]); 2094 let blinded = ledger 2095 .build_blinded_burn(&dave, 3, 7, ledger.height() + 4) 2096 .unwrap(); 2097 ledger 2098 .submit_blinded_transaction(blinded.transaction.clone()) 2099 .unwrap(); 2100 queue_next_leader_burn(&mut ledger, &finalizers); 2101 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2102 ledger 2103 .submit_blinded_reveal(blinded.reveal.clone()) 2104 .unwrap(); 2105 queue_next_leader_burn(&mut ledger, &finalizers); 2106 2107 let mut bundles = ledger 2108 .reveal_committee_for_next_block() 2109 .into_iter() 2110 .filter_map(|member| { 2111 let wallet = wallet_for_address(&finalizers, &member.owner); 2112 ledger.build_reveal_bundle(wallet).unwrap() 2113 }) 2114 .collect::<Vec<_>>(); 2115 assert!(bundles.len() >= 2); 2116 bundles.truncate(2); 2117 let expected_hashes = reveal_bundle_hashes(&bundles); 2118 let expected_mask = bundles 2119 .iter() 2120 .fold(0_u8, |mask, bundle| mask | (1_u8 << bundle.slot)); 2121 2122 let leader = ledger.expected_leader_for_next_block().unwrap(); 2123 let leader_wallet = wallet_for_address(&finalizers, &leader); 2124 let prepared = ledger 2125 .prepare_next_block_with_reveal_bundles( 2126 leader_wallet.address(), 2127 ledger.tip().timestamp_ms + 1, 2128 bundles.clone(), 2129 ) 2130 .unwrap(); 2131 let block = prepared.finish(leader_wallet, "preverified-vdf".to_string()); 2132 2133 assert_eq!(block.reveal_bundle_section.signatures.len(), 2); 2134 assert_eq!(block.reveal_bundle_section.reveals.len(), 1); 2135 assert_eq!( 2136 block.reveal_bundle_section.reveals[0].bundle_mask, 2137 expected_mask 2138 ); 2139 assert_eq!(block.all_blinded_reveals(), vec![&blinded.reveal]); 2140 assert_eq!(block.reveal_bundle_hashes(), expected_hashes); 2141 assert_eq!( 2142 block 2143 .reveal_bundle_section 2144 .expand(block.height, &block.prev_hash), 2145 bundles 2146 ); 2147 } 2148 2149 fn ledger_with_duplicate_owner_reveal_tickets() -> Ledger { 2150 let alice = Wallet::from_seed("bundle-unique-owner-alice"); 2151 let bob = Wallet::from_seed("bundle-unique-owner-bob"); 2152 let mut ledger = Ledger::new(BTreeMap::new(), 1); 2153 ledger.tickets = vec![ 2154 BurnTicket { 2155 id: "alice-0".to_string(), 2156 owner: alice.address().to_string(), 2157 amount: MICRO_IUNA, 2158 eligible_from_height: 499, 2159 eligible_until_height: 500, 2160 }, 2161 BurnTicket { 2162 id: "alice-1".to_string(), 2163 owner: alice.address().to_string(), 2164 amount: MICRO_IUNA, 2165 eligible_from_height: 499, 2166 eligible_until_height: 500, 2167 }, 2168 BurnTicket { 2169 id: "bob-0".to_string(), 2170 owner: bob.address().to_string(), 2171 amount: MICRO_IUNA, 2172 eligible_from_height: 499, 2173 eligible_until_height: 500, 2174 }, 2175 ]; 2176 ledger 2177 } 2178 2179 #[test] 2180 fn reveal_committee_allows_duplicate_owners_before_height_500() { 2181 let ledger = ledger_with_duplicate_owner_reveal_tickets(); 2182 2183 let committee = ledger.reveal_committee_for_height(499); 2184 let owners = committee 2185 .iter() 2186 .map(|member| member.owner.as_str()) 2187 .collect::<BTreeSet<_>>(); 2188 2189 assert_eq!(committee.len(), 3); 2190 assert_eq!(owners.len(), 2); 2191 } 2192 2193 #[test] 2194 fn reveal_committee_uses_unique_ticket_owners_from_height_500() { 2195 let ledger = ledger_with_duplicate_owner_reveal_tickets(); 2196 2197 let committee = ledger.reveal_committee_for_height(500); 2198 let owners = committee 2199 .iter() 2200 .map(|member| member.owner.as_str()) 2201 .collect::<BTreeSet<_>>(); 2202 2203 assert_eq!(committee.len(), 2); 2204 assert_eq!(owners.len(), 2); 2205 assert_eq!(committee.first().map(|member| member.rank), Some(0)); 2206 } 2207 2208 #[test] 2209 fn reveal_bundle_validation_rejects_wrong_signature_and_slot() { 2210 let alice = Wallet::from_seed("bundle-invalid-alice"); 2211 let bob = Wallet::from_seed("bundle-invalid-bob"); 2212 let carol = Wallet::from_seed("bundle-invalid-carol"); 2213 let finalizers = [alice.clone(), bob.clone()]; 2214 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2215 let blinded = ledger 2216 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2217 .unwrap(); 2218 ledger 2219 .submit_blinded_transaction(blinded.transaction.clone()) 2220 .unwrap(); 2221 queue_next_leader_burn(&mut ledger, &finalizers); 2222 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2223 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2224 let member = ledger.reveal_committee_for_next_block()[0].clone(); 2225 let wallet = wallet_for_address(&finalizers, &member.owner); 2226 let bundle = ledger.build_reveal_bundle(wallet).unwrap().unwrap(); 2227 2228 let mut wrong_signature = bundle.clone(); 2229 wrong_signature.signature = "00".repeat(SIGNATURE_BYTES); 2230 let error = ledger 2231 .validate_next_block_reveal_bundles(vec![wrong_signature]) 2232 .unwrap_err(); 2233 assert!(format!("{error:#}").contains("reveal bundle signature is invalid")); 2234 2235 let mut wrong_slot = bundle; 2236 wrong_slot.slot = REVEAL_COMMITTEE_SIZE as u8 - 1; 2237 let error = ledger 2238 .validate_next_block_reveal_bundles(vec![wrong_slot]) 2239 .unwrap_err(); 2240 assert!( 2241 format!("{error:#}").contains("reveal bundle slot is not assigned") 2242 || format!("{error:#}").contains("reveal bundle member is not assigned to slot") 2243 ); 2244 } 2245 2246 #[test] 2247 fn blinded_reveal_with_wrong_key_is_rejected_in_block() { 2248 let alice = Wallet::from_seed("blinded-wrong-key-finalizer-alice"); 2249 let bob = Wallet::from_seed("blinded-wrong-key-finalizer-bob"); 2250 let carol = Wallet::from_seed("blinded-wrong-key-carol"); 2251 let finalizers = [alice.clone(), bob.clone()]; 2252 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2253 let blinded = ledger 2254 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2255 .unwrap(); 2256 ledger 2257 .submit_blinded_transaction(blinded.transaction.clone()) 2258 .unwrap(); 2259 queue_next_leader_burn(&mut ledger, &finalizers); 2260 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2261 2262 let leader = ledger.expected_leader_for_next_block().unwrap(); 2263 let wallet = wallet_for_address(&finalizers, &leader); 2264 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2265 ledger.submit_transaction(filler_burn).unwrap(); 2266 let mut prepared = ledger 2267 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 2268 .unwrap(); 2269 let committee_member = ledger.reveal_committee_for_next_block()[0].clone(); 2270 let committee_wallet = wallet_for_address(&finalizers, &committee_member.owner); 2271 let wrong_reveal = BlindedReveal { 2272 commitment: blinded.transaction.commitment, 2273 key: "00".repeat(BLINDED_KEY_BYTES), 2274 }; 2275 let wrong_bundle = committee_wallet.reveal_bundle(RevealBundlePayload { 2276 height: prepared.height, 2277 prev_hash: prepared.prev_hash.clone(), 2278 slot: committee_member.slot, 2279 member: committee_wallet.address().to_string(), 2280 reveals: vec![wrong_reveal], 2281 }); 2282 prepared.reveal_bundle_section = ledger.reveal_bundle_section_from_bundles(vec![wrong_bundle]); 2283 prepared.reward = blinded_reveal_finalizer_fee( 2284 blinded.transaction.fee, 2285 prepared.reveal_bundle_section.signatures.len(), 2286 ledger.reveal_committee_for_next_block().len(), 2287 ); 2288 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 2289 2290 let error = ledger 2291 .apply_preverified_block_at(block, u64::MAX) 2292 .unwrap_err(); 2293 2294 assert!( 2295 format!("{error:#}").contains("decrypt blinded transaction"), 2296 "{error:#}" 2297 ); 2298 } 2299 2300 #[test] 2301 fn expired_blinded_reveal_is_not_selected() { 2302 let alice = Wallet::from_seed("blinded-expire-finalizer-alice"); 2303 let bob = Wallet::from_seed("blinded-expire-finalizer-bob"); 2304 let carol = Wallet::from_seed("blinded-expire-carol"); 2305 let finalizers = [alice.clone(), bob.clone()]; 2306 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2307 let blinded = ledger 2308 .build_blinded_burn(&carol, 3, 7, ledger.height() + 2) 2309 .unwrap(); 2310 ledger 2311 .submit_blinded_transaction(blinded.transaction.clone()) 2312 .unwrap(); 2313 queue_next_leader_burn(&mut ledger, &finalizers); 2314 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2315 2316 let leader = ledger.expected_leader_for_next_block().unwrap(); 2317 let wallet = wallet_for_address(&finalizers, &leader); 2318 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2319 ledger.submit_transaction(filler_burn).unwrap(); 2320 mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); 2321 2322 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2323 assert!(ledger.valid_pending_blinded_reveals().is_empty()); 2324 } 2325 2326 #[test] 2327 fn active_blinded_reveal_displaces_invalid_reveal_spam_when_pool_is_full() { 2328 let alice = Wallet::from_seed("blinded-spam-finalizer-alice"); 2329 let bob = Wallet::from_seed("blinded-spam-finalizer-bob"); 2330 let carol = Wallet::from_seed("blinded-spam-carol"); 2331 let finalizers = [alice.clone(), bob.clone()]; 2332 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2333 let blinded = ledger 2334 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2335 .unwrap(); 2336 ledger 2337 .submit_blinded_transaction(blinded.transaction.clone()) 2338 .unwrap(); 2339 queue_next_leader_burn(&mut ledger, &finalizers); 2340 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2341 2342 for index in 0..MAX_PENDING_TRANSACTIONS { 2343 ledger 2344 .submit_blinded_reveal(BlindedReveal { 2345 commitment: hex_hash(format!("unknown-blinded-reveal-spam:{index}")), 2346 key: "00".repeat(BLINDED_KEY_BYTES), 2347 }) 2348 .unwrap(); 2349 } 2350 assert_eq!( 2351 ledger.pending_blinded_reveals().len(), 2352 MAX_PENDING_TRANSACTIONS 2353 ); 2354 assert!(ledger.valid_pending_blinded_reveals().is_empty()); 2355 2356 assert!( 2357 ledger 2358 .submit_blinded_reveal(blinded.reveal.clone()) 2359 .unwrap() 2360 ); 2361 2362 assert_eq!( 2363 ledger.pending_blinded_reveals().len(), 2364 MAX_PENDING_TRANSACTIONS 2365 ); 2366 assert_eq!(ledger.valid_pending_blinded_reveals(), vec![blinded.reveal]); 2367 } 2368 2369 #[test] 2370 fn recovery_block_includes_pending_blinded_transactions_when_space_allows() { 2371 let alice = Wallet::from_seed("recovery-blinded-commit-alice"); 2372 let bob = Wallet::from_seed("recovery-blinded-commit-bob"); 2373 let carol = Wallet::from_seed("recovery-blinded-commit-carol"); 2374 let mut ledger = ledger_with_finalizers( 2375 &[alice], 2376 &[(&bob, 10 * MICRO_IUNA), (&carol, 10 * MICRO_IUNA)], 2377 ); 2378 let blinded = ledger 2379 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2380 .unwrap(); 2381 ledger 2382 .submit_blinded_transaction(blinded.transaction.clone()) 2383 .unwrap(); 2384 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2385 ledger.submit_transaction(recovery_burn).unwrap(); 2386 2387 let block = ledger 2388 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 2389 .unwrap(); 2390 2391 assert!( 2392 block 2393 .blinded_transactions 2394 .iter() 2395 .any(|transaction| transaction.commitment == blinded.transaction.commitment) 2396 ); 2397 } 2398 2399 #[test] 2400 fn recovery_block_size_selection_uses_recovery_skeleton() { 2401 let alice = Wallet::from_seed("recovery-size-commit-alice"); 2402 let bob = Wallet::from_seed("recovery-size-commit-bob"); 2403 let carol = Wallet::from_seed("recovery-size-commit-carol"); 2404 let mut ledger = ledger_with_finalizers( 2405 &[alice], 2406 &[(&bob, 10 * MICRO_IUNA), (&carol, 10 * MICRO_IUNA)], 2407 ); 2408 let blinded = ledger 2409 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2410 .unwrap(); 2411 ledger 2412 .submit_blinded_transaction(blinded.transaction.clone()) 2413 .unwrap(); 2414 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2415 ledger.submit_transaction(recovery_burn.clone()).unwrap(); 2416 let recovery_selection = BlockSelection { 2417 transactions: vec![recovery_burn], 2418 blinded_transactions: vec![blinded.transaction.clone()], 2419 }; 2420 let recovery_estimate = 2421 estimated_block_selection_size_bytes(&recovery_selection, true).unwrap(); 2422 let ticket_estimate = estimated_block_selection_size_bytes(&recovery_selection, false).unwrap(); 2423 assert!(recovery_estimate < ticket_estimate); 2424 2425 ledger.launch_profile.max_block_bytes = recovery_estimate; 2426 let tight_block = ledger 2427 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 2428 .unwrap(); 2429 2430 assert!( 2431 tight_block 2432 .blinded_transactions 2433 .iter() 2434 .any(|transaction| transaction.commitment == blinded.transaction.commitment) 2435 ); 2436 } 2437 2438 #[test] 2439 fn recovery_block_includes_pending_blinded_reveals_when_space_allows() { 2440 let alice = Wallet::from_seed("recovery-blinded-reveal-alice"); 2441 let bob = Wallet::from_seed("recovery-blinded-reveal-bob"); 2442 let carol = Wallet::from_seed("recovery-blinded-reveal-carol"); 2443 let finalizers = [alice.clone(), bob.clone()]; 2444 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2445 let blinded = ledger 2446 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2447 .unwrap(); 2448 ledger 2449 .submit_blinded_transaction(blinded.transaction.clone()) 2450 .unwrap(); 2451 queue_next_leader_burn(&mut ledger, &finalizers); 2452 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2453 ledger 2454 .submit_blinded_reveal(blinded.reveal.clone()) 2455 .unwrap(); 2456 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2457 ledger.submit_transaction(recovery_burn).unwrap(); 2458 2459 let bundles = ledger 2460 .reveal_committee_for_next_block() 2461 .into_iter() 2462 .filter_map(|member| { 2463 let wallet = wallet_for_address(&finalizers, &member.owner); 2464 ledger.build_reveal_bundle(wallet).unwrap() 2465 }) 2466 .collect::<Vec<_>>(); 2467 let prepared = ledger 2468 .prepare_recovery_block_with_reveal_bundles( 2469 bob.address(), 2470 ledger.recovery_block_min_timestamp(), 2471 bundles, 2472 ) 2473 .unwrap(); 2474 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 2475 let block = prepared.finish(&bob, vdf_output); 2476 2477 assert!( 2478 block 2479 .all_blinded_reveals() 2480 .iter() 2481 .any(|reveal| reveal.commitment == blinded.transaction.commitment) 2482 ); 2483 } 2484 2485 #[test] 2486 fn blinded_transaction_expiring_at_next_height_is_not_selected() { 2487 let alice = Wallet::from_seed("blinded-next-expire-finalizer-alice"); 2488 let bob = Wallet::from_seed("blinded-next-expire-finalizer-bob"); 2489 let carol = Wallet::from_seed("blinded-next-expire-carol"); 2490 let finalizers = [alice.clone(), bob.clone()]; 2491 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2492 let blinded = ledger 2493 .build_blinded_burn(&carol, 3, 7, ledger.height() + 1) 2494 .unwrap(); 2495 ledger 2496 .submit_blinded_transaction(blinded.transaction) 2497 .unwrap(); 2498 2499 let leader = ledger.expected_leader_for_next_block().unwrap(); 2500 let wallet = wallet_for_address(&finalizers, &leader); 2501 let error = ledger.prepare_next_block(wallet.address(), 1).unwrap_err(); 2502 2503 assert!(format!("{error:#}").contains("block must include at least one burn transaction")); 2504 } 2505 2506 #[test] 2507 fn blinded_transaction_expiry_cannot_exceed_protocol_window() { 2508 let alice = Wallet::from_seed("blinded-window-finalizer-alice"); 2509 let bob = Wallet::from_seed("blinded-window-finalizer-bob"); 2510 let carol = Wallet::from_seed("blinded-window-carol"); 2511 let finalizers = [alice, bob]; 2512 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2513 let max_expiry = ledger 2514 .height() 2515 .saturating_add(MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS); 2516 2517 ledger.build_blinded_burn(&carol, 3, 7, max_expiry).unwrap(); 2518 2519 let error = ledger 2520 .build_blinded_burn(&carol, 3, 7, max_expiry + 1) 2521 .unwrap_err(); 2522 assert!(format!("{error:#}").contains("expiry is too far in the future")); 2523 2524 let mut forged = ledger 2525 .build_blinded_burn(&carol, 3, 7, max_expiry) 2526 .unwrap() 2527 .transaction; 2528 forged.expires_at_height = max_expiry + 1; 2529 forged.commitment = blinded_transaction_commitment(&forged).unwrap(); 2530 let error = ledger.submit_blinded_transaction(forged).unwrap_err(); 2531 assert!(format!("{error:#}").contains("expiry is too far in the future")); 2532 } 2533 2534 #[test] 2535 fn blinded_transaction_does_not_satisfy_plaintext_burn_requirement() { 2536 let alice = Wallet::from_seed("blinded-no-burn-finalizer-alice"); 2537 let bob = Wallet::from_seed("blinded-no-burn-finalizer-bob"); 2538 let carol = Wallet::from_seed("blinded-no-burn-carol"); 2539 let finalizers = [alice.clone(), bob.clone()]; 2540 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2541 let blinded = ledger 2542 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2543 .unwrap(); 2544 ledger 2545 .submit_blinded_transaction(blinded.transaction) 2546 .unwrap(); 2547 2548 let leader = ledger.expected_leader_for_next_block().unwrap(); 2549 let wallet = wallet_for_address(&finalizers, &leader); 2550 let error = ledger.prepare_next_block(wallet.address(), 1).unwrap_err(); 2551 2552 assert!(format!("{error:#}").contains("block must include at least one burn transaction")); 2553 } 2554 2555 #[test] 2556 fn revealed_blinded_transaction_cannot_be_included_again() { 2557 let alice = Wallet::from_seed("blinded-duplicate-finalizer-alice"); 2558 let bob = Wallet::from_seed("blinded-duplicate-finalizer-bob"); 2559 let carol = Wallet::from_seed("blinded-duplicate-carol"); 2560 let finalizers = [alice.clone(), bob.clone()]; 2561 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2562 let blinded = ledger 2563 .build_blinded_burn(&carol, 3, 7, ledger.height() + 6) 2564 .unwrap(); 2565 ledger 2566 .submit_blinded_transaction(blinded.transaction.clone()) 2567 .unwrap(); 2568 queue_next_leader_burn(&mut ledger, &finalizers); 2569 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2570 ledger 2571 .submit_blinded_reveal(blinded.reveal.clone()) 2572 .unwrap(); 2573 queue_next_leader_burn(&mut ledger, &finalizers); 2574 mine_preverified_as_next_leader_with_reveal_bundles(&mut ledger, &finalizers, 2); 2575 2576 let leader = ledger.expected_leader_for_next_block().unwrap(); 2577 let wallet = wallet_for_address(&finalizers, &leader); 2578 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2579 ledger.submit_transaction(filler_burn).unwrap(); 2580 let mut prepared = ledger 2581 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 2582 .unwrap(); 2583 prepared 2584 .blinded_transactions 2585 .push(blinded.transaction.clone()); 2586 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 2587 2588 let error = ledger 2589 .apply_preverified_block_at(block, u64::MAX) 2590 .unwrap_err(); 2591 2592 assert!(format!("{error:#}").contains("blinded transaction is already on chain")); 2593 } 2594 2595 #[test] 2596 fn pending_blinded_reveals_expose_revealed_transaction_data() { 2597 let alice = Wallet::from_seed("pending-reveal-data-finalizer-alice"); 2598 let bob = Wallet::from_seed("pending-reveal-data-finalizer-bob"); 2599 let carol = Wallet::from_seed("pending-reveal-data-carol"); 2600 let finalizers = [alice.clone(), bob.clone()]; 2601 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2602 let blinded = ledger 2603 .build_blinded_burn(&carol, 3, 7, ledger.height() + 6) 2604 .unwrap(); 2605 let commitment = blinded.transaction.commitment.clone(); 2606 ledger 2607 .submit_blinded_transaction(blinded.transaction) 2608 .unwrap(); 2609 queue_next_leader_burn(&mut ledger, &finalizers); 2610 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2611 2612 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2613 2614 let revealed = ledger.pending_revealed_blinded_transactions(); 2615 assert_eq!(revealed.len(), 1); 2616 assert_eq!(revealed[0].commitment, commitment); 2617 assert_eq!(revealed[0].height, ledger.height() + 1); 2618 assert_eq!(revealed[0].transaction.amount(), 3); 2619 assert_eq!(revealed[0].transaction.fee(), 7); 2620 assert!(revealed[0].transaction.is_burn()); 2621 } 2622 2623 #[test] 2624 fn abandoned_fork_blinded_transactions_return_to_mempool() { 2625 let alice = Wallet::from_seed("blinded-reorg-finalizer-alice"); 2626 let bob = Wallet::from_seed("blinded-reorg-finalizer-bob"); 2627 let carol = Wallet::from_seed("blinded-reorg-carol"); 2628 let finalizers = [alice.clone(), bob.clone()]; 2629 let mut local = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2630 let mut remote = local.clone(); 2631 let blinded = local 2632 .build_blinded_burn(&carol, 3, 7, local.height() + 8) 2633 .unwrap(); 2634 local 2635 .submit_blinded_transaction(blinded.transaction.clone()) 2636 .unwrap(); 2637 queue_next_leader_burn(&mut local, &finalizers); 2638 mine_preverified_as_next_leader(&mut local, &finalizers, 1); 2639 2640 for timestamp_ms in [1, 2] { 2641 let leader = remote.expected_leader_for_next_block().unwrap(); 2642 let wallet = wallet_for_address(&finalizers, &leader); 2643 let burn = remote.build_burn(wallet, 1, 0).unwrap(); 2644 remote.submit_transaction(burn).unwrap(); 2645 mine_preverified_as_next_leader(&mut remote, &finalizers, timestamp_ms); 2646 } 2647 2648 assert!( 2649 local 2650 .extend_from_preverified_snapshot_at(remote.snapshot(), u64::MAX) 2651 .unwrap() 2652 ); 2653 assert!(local.has_blinded_transaction(&blinded.transaction.commitment)); 2654 assert_eq!( 2655 local.pending_blinded_transactions(), 2656 std::slice::from_ref(&blinded.transaction) 2657 ); 2658 } 2659 2660 #[test] 2661 fn block_selection_includes_mine_action_after_required_block_burn() { 2662 let alice = Wallet::from_seed("mine-fixed-reward-select-alice"); 2663 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2664 2665 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2666 ledger.submit_transaction(burn).unwrap(); 2667 let mine = ledger.build_mine(alice.address()).unwrap(); 2668 ledger.submit_transaction(mine.clone()).unwrap(); 2669 2670 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2671 2672 assert_eq!( 2673 block.transactions.iter().filter(|tx| tx.is_burn()).count(), 2674 1 2675 ); 2676 assert!( 2677 block 2678 .transactions 2679 .iter() 2680 .any(|tx| tx.signature() == mine.signature()) 2681 ); 2682 assert_eq!(block.reward, MINE_FINALIZER_FEE); 2683 } 2684 2685 #[test] 2686 fn block_selection_can_skip_mine_action_when_space_is_limited() { 2687 let alice = Wallet::from_seed("mine-space-limit-alice"); 2688 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2689 ledger.launch_profile.max_block_transactions = 2; 2690 2691 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2692 ledger.submit_transaction(burn).unwrap(); 2693 let first_mine = ledger.build_mine(alice.address()).unwrap(); 2694 ledger.submit_transaction(first_mine).unwrap(); 2695 let second_mine = ledger.build_mine(alice.address()).unwrap(); 2696 ledger.submit_transaction(second_mine).unwrap(); 2697 2698 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2699 2700 assert_eq!(block.transactions.len(), 2); 2701 assert!(block.transactions.iter().any(Transaction::is_burn)); 2702 assert_eq!(block.reward, MINE_FINALIZER_FEE); 2703 } 2704 2705 #[test] 2706 fn pending_mine_outputs_are_not_spendable_until_confirmed() { 2707 let alice = Wallet::from_seed("pending-mine-spend-alice"); 2708 let bob = Wallet::from_seed("pending-mine-spend-bob"); 2709 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2710 2711 let mine = ledger.build_mine(alice.address()).unwrap(); 2712 let mine_outpoint = OutPoint { 2713 txid: mine.signature().to_string(), 2714 index: 0, 2715 }; 2716 ledger.submit_transaction(mine.clone()).unwrap(); 2717 2718 assert!( 2719 !ledger 2720 .available_utxos_for_address(alice.address()) 2721 .unwrap() 2722 .iter() 2723 .any(|(outpoint, _)| outpoint == &mine_outpoint) 2724 ); 2725 let pending_error = ledger 2726 .build_transfer_with_inputs( 2727 &alice, 2728 bob.address(), 2729 TEST_BURN_AMOUNT, 2730 0, 2731 std::slice::from_ref(&mine_outpoint), 2732 ) 2733 .unwrap_err(); 2734 assert!(format!("{pending_error:#}").contains("not spendable")); 2735 2736 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2737 ledger.submit_transaction(burn).unwrap(); 2738 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2739 assert!( 2740 block 2741 .transactions 2742 .iter() 2743 .any(|tx| tx.signature() == mine.signature()) 2744 ); 2745 ledger.apply_locally_mined_block(block).unwrap(); 2746 2747 assert!( 2748 ledger 2749 .available_utxos_for_address(alice.address()) 2750 .unwrap() 2751 .iter() 2752 .any(|(outpoint, _)| outpoint == &mine_outpoint) 2753 ); 2754 ledger 2755 .build_transfer_with_inputs( 2756 &alice, 2757 bob.address(), 2758 TEST_BURN_AMOUNT, 2759 0, 2760 std::slice::from_ref(&mine_outpoint), 2761 ) 2762 .unwrap(); 2763 } 2764 2765 #[test] 2766 fn burns_built_after_pending_mine_do_not_spend_pending_mine_output() { 2767 let alice = Wallet::from_seed("pending-mine-burn-alice"); 2768 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2769 2770 let mine = ledger.build_mine(alice.address()).unwrap(); 2771 let mine_outpoint = OutPoint { 2772 txid: mine.signature().to_string(), 2773 index: 0, 2774 }; 2775 ledger.submit_transaction(mine).unwrap(); 2776 2777 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2778 2779 let Transaction::Burn { inputs, .. } = &burn else { 2780 panic!("expected burn transaction"); 2781 }; 2782 assert!(!inputs.iter().any(|input| input.outpoint == mine_outpoint)); 2783 } 2784 2785 #[test] 2786 fn pending_blinded_transactions_with_spent_inputs_are_pruned_after_block_apply() { 2787 let alice = Wallet::from_seed("pending-blind-spent-prune-alice"); 2788 let mut mempool_ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2789 let mut block_ledger = mempool_ledger.clone(); 2790 let amount = mempool_ledger.balance_of(alice.address()); 2791 let blinded = mempool_ledger 2792 .build_blinded_burn(&alice, amount, 0, mempool_ledger.height() + 4) 2793 .unwrap(); 2794 mempool_ledger 2795 .submit_blinded_transaction(blinded.transaction.clone()) 2796 .unwrap(); 2797 2798 let burn = block_ledger.build_burn(&alice, amount, 0).unwrap(); 2799 let Transaction::Burn { inputs, .. } = &burn else { 2800 panic!("expected burn transaction"); 2801 }; 2802 assert!(blinded.transaction.inputs.iter().any(|input| { 2803 inputs 2804 .iter() 2805 .any(|burn_input| burn_input.outpoint == input.outpoint) 2806 })); 2807 block_ledger.submit_transaction(burn).unwrap(); 2808 let block = block_ledger.mine_next_block(&alice, 1).unwrap(); 2809 2810 mempool_ledger.apply_block(block).unwrap(); 2811 2812 assert!(mempool_ledger.pending_blinded_transactions().is_empty()); 2813 } 2814 2815 #[test] 2816 fn block_selection_limits_mine_actions_per_anchor() { 2817 let alice = Wallet::from_seed("mine-anchor-limit-selection-alice"); 2818 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2819 2820 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2821 ledger.submit_transaction(burn).unwrap(); 2822 let first_mine = ledger.build_mine(alice.address()).unwrap(); 2823 ledger.submit_transaction(first_mine.clone()).unwrap(); 2824 let second_mine = ledger.build_mine(alice.address()).unwrap(); 2825 ledger.submit_transaction(second_mine.clone()).unwrap(); 2826 assert_eq!(ledger.pending().len(), 3); 2827 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2828 2829 assert_eq!(block.transactions.len(), 3); 2830 assert!(block.transactions.iter().any(Transaction::is_burn)); 2831 let included_mines = block 2832 .transactions 2833 .iter() 2834 .filter(|transaction| matches!(transaction, Transaction::Mine { .. })) 2835 .count(); 2836 assert_eq!(included_mines, MINE_ACTIONS_PER_ANCHOR_LIMIT); 2837 assert!( 2838 block 2839 .transactions 2840 .iter() 2841 .any(|tx| tx.signature() == first_mine.signature()) 2842 ); 2843 assert!( 2844 block 2845 .transactions 2846 .iter() 2847 .any(|tx| tx.signature() == second_mine.signature()) 2848 ); 2849 assert_ne!(first_mine.signature(), second_mine.signature()); 2850 assert_eq!(block.reward, first_mine.fee() + second_mine.fee()); 2851 } 2852 2853 #[test] 2854 fn blocks_reject_too_many_mine_actions_for_one_anchor() { 2855 let alice = Wallet::from_seed("mine-anchor-limit-block-alice"); 2856 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2857 2858 let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); 2859 let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); 2860 let third_mine = test_mine_with_salt(&ledger, alice.address(), 3); 2861 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2862 ledger.submit_transaction(burn).unwrap(); 2863 let mut block = ledger 2864 .prepare_next_block( 2865 alice.address(), 2866 ledger 2867 .tip() 2868 .timestamp_ms 2869 .saturating_add(VDF_TARGET_BLOCK_MS), 2870 ) 2871 .unwrap() 2872 .finish(&alice, "preverified-vdf".to_string()); 2873 block.transactions.push(first_mine); 2874 block.transactions.push(second_mine); 2875 block.transactions.push(third_mine); 2876 block.reward = fee_reward(&block.transactions).unwrap(); 2877 block.hash = block.compute_hash(); 2878 2879 let error = ledger 2880 .apply_preverified_block_at(block, u64::MAX) 2881 .unwrap_err(); 2882 2883 assert!(format!("{error:#}").contains("mine actions per anchor limit")); 2884 } 2885 2886 #[test] 2887 fn mempool_rejects_mine_actions_above_anchor_limit() { 2888 let alice = Wallet::from_seed("mine-anchor-limit-mempool-alice"); 2889 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2890 2891 let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); 2892 let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); 2893 let third_mine = test_mine_with_salt(&ledger, alice.address(), 3); 2894 ledger.submit_transaction(first_mine).unwrap(); 2895 ledger.submit_transaction(second_mine).unwrap(); 2896 let error = ledger.submit_transaction(third_mine).unwrap_err(); 2897 2898 assert!(format!("{error:#}").contains("mine transaction anchor limit reached")); 2899 } 2900 2901 #[test] 2902 fn mine_difficulty_increases_when_issuance_exceeds_target_window() { 2903 let alice = Wallet::from_seed("mine-difficulty-up-alice"); 2904 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2905 2906 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2907 apply_preverified_burn_block_with_mines(&mut ledger, &alice, 2); 2908 } 2909 2910 assert_eq!( 2911 ledger.current_mine_difficulty_bits(), 2912 MINE_DIFFICULTY_BITS + 1 2913 ); 2914 let mine = ledger.build_mine(alice.address()).unwrap(); 2915 let Transaction::Mine { 2916 difficulty_bits, .. 2917 } = mine 2918 else { 2919 panic!("expected mine action"); 2920 }; 2921 assert_eq!(difficulty_bits, MINE_DIFFICULTY_BITS + 1); 2922 } 2923 2924 #[test] 2925 fn mine_difficulty_decreases_when_issuance_is_below_target_window() { 2926 let alice = Wallet::from_seed("mine-difficulty-down-alice"); 2927 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2928 2929 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2930 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2931 } 2932 2933 assert_eq!( 2934 ledger.current_mine_difficulty_bits(), 2935 MINE_DIFFICULTY_BITS - MINE_MAX_RETARGET_STEP_BITS 2936 ); 2937 2938 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2939 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2940 } 2941 2942 assert_eq!( 2943 ledger.current_mine_difficulty_bits(), 2944 MINE_MIN_DIFFICULTY_BITS 2945 ); 2946 2947 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2948 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2949 } 2950 2951 assert_eq!( 2952 ledger.current_mine_difficulty_bits(), 2953 MINE_MIN_DIFFICULTY_BITS 2954 ); 2955 } 2956 2957 #[test] 2958 fn mine_actions_expire_when_anchor_is_too_old() { 2959 let alice = Wallet::from_seed("mine-anchor-expiry-alice"); 2960 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2961 let stale_mine = ledger.build_mine(alice.address()).unwrap(); 2962 2963 for _ in 0..=MINE_MAX_ANCHOR_AGE_BLOCKS { 2964 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2965 } 2966 2967 let error = ledger.submit_transaction(stale_mine).unwrap_err(); 2968 assert!(format!("{error:#}").contains("mine transaction anchor is too old")); 2969 } 2970 2971 #[test] 2972 fn pending_mine_actions_are_removed_when_anchor_expires() { 2973 let alice = Wallet::from_seed("pending-mine-anchor-expiry-alice"); 2974 let bob = Wallet::from_seed("pending-mine-anchor-expiry-bob"); 2975 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2976 ledger.launch_profile.max_block_transactions = 1; 2977 let stale_mine = ledger.build_mine(bob.address()).unwrap(); 2978 ledger.submit_transaction(stale_mine.clone()).unwrap(); 2979 2980 for _ in 0..=MINE_MAX_ANCHOR_AGE_BLOCKS { 2981 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2982 } 2983 2984 assert!( 2985 ledger 2986 .pending() 2987 .iter() 2988 .all(|tx| tx.signature() != stale_mine.signature()) 2989 ); 2990 } 2991 2992 #[test] 2993 fn stratum_mine_header_proof_is_validated() { 2994 let alice = Wallet::from_seed("stratum-proof-alice"); 2995 let ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2996 let anchor = ledger.tip().hash.clone(); 2997 let difficulty_bits = ledger.current_mine_difficulty_bits(); 2998 let template = ledger 2999 .stratum_mine_template(alice.address(), anchor, 1, difficulty_bits) 3000 .unwrap(); 3001 3002 let mut accepted = None; 3003 for nonce in 0_u32..50_000 { 3004 let result = ledger.build_stratum_mine( 3005 template.clone(), 3006 StratumMineShare { 3007 extranonce2: [0, 0, 0, 0], 3008 header_nonce: nonce.to_le_bytes(), 3009 }, 3010 ); 3011 if let Ok(tx) = result { 3012 accepted = Some(tx); 3013 break; 3014 } 3015 } 3016 3017 let tx = accepted.expect("expected Stratum proof within search range"); 3018 let Transaction::Mine { 3019 proof_header, 3020 signature, 3021 .. 3022 } = tx 3023 else { 3024 panic!("expected mine action"); 3025 }; 3026 assert_eq!(proof_header.as_deref().unwrap_or_default().len(), 160); 3027 assert!(hash_meets_difficulty(&signature, difficulty_bits)); 3028 } 3029 3030 #[test] 3031 fn stratum_mine_salt_allows_multiple_actions_for_same_anchor() { 3032 let alice = Wallet::from_seed("stratum-salt-alice"); 3033 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 3034 let anchor = ledger.tip().hash.clone(); 3035 let difficulty_bits = ledger.current_mine_difficulty_bits(); 3036 3037 for salt in [1, 2] { 3038 let template = ledger 3039 .stratum_mine_template(alice.address(), anchor.clone(), salt, difficulty_bits) 3040 .unwrap(); 3041 let mut accepted = None; 3042 for nonce in 0_u32..50_000 { 3043 let result = ledger.build_stratum_mine( 3044 template.clone(), 3045 StratumMineShare { 3046 extranonce2: [0, 0, 0, 0], 3047 header_nonce: nonce.to_le_bytes(), 3048 }, 3049 ); 3050 if let Ok(tx) = result { 3051 accepted = Some(tx); 3052 break; 3053 } 3054 } 3055 let tx = accepted.expect("expected Stratum proof within search range"); 3056 assert!(ledger.submit_transaction(tx).unwrap()); 3057 } 3058 3059 assert_eq!(ledger.pending().len(), 2); 3060 let salts = ledger 3061 .pending() 3062 .iter() 3063 .map(|tx| match tx { 3064 Transaction::Mine { salt, .. } => *salt, 3065 _ => panic!("expected mine action"), 3066 }) 3067 .collect::<BTreeSet<_>>(); 3068 assert_eq!(salts, BTreeSet::from([1, 2])); 3069 }