tests.rs (96687B)
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 transfer_rejects_invalid_recipient_address() { 618 let alice = Wallet::from_seed("invalid-transfer-recipient-alice"); 619 let ledger = ledger_with_wallet_utxos(&alice, &[10]); 620 621 let error = ledger.build_transfer(&alice, "aa", 1, 0).unwrap_err(); 622 623 assert!(format!("{error:#}").contains("invalid transfer recipient address")); 624 } 625 626 #[test] 627 fn mine_rejects_invalid_recipient_address_before_pow() { 628 let ledger = Ledger::new(BTreeMap::new(), 1); 629 630 let error = ledger.build_mine("aa").unwrap_err(); 631 632 assert!(format!("{error:#}").contains("invalid mine recipient address")); 633 } 634 635 #[test] 636 fn mine_search_respects_nonce_attempt_limit() { 637 let alice = Wallet::from_seed("bounded-mine-search-alice"); 638 let ledger = Ledger::new(BTreeMap::new(), 1); 639 640 let outcome = ledger.search_mine(alice.address(), 1, 0, 0).unwrap(); 641 642 assert!(outcome.transaction.is_none()); 643 assert_eq!(outcome.next_nonce, 0); 644 assert_eq!(outcome.attempts, 0); 645 } 646 647 #[test] 648 fn mempool_rejects_invalid_input_outpoint_id() { 649 let alice = Wallet::from_seed("invalid-outpoint-alice"); 650 let bob = Wallet::from_seed("invalid-outpoint-bob"); 651 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 652 let unsigned = UnsignedUtxoTransaction::Transfer { 653 inputs: vec![UnsignedTxInput { 654 outpoint: OutPoint { 655 txid: "aa".to_string(), 656 index: 0, 657 }, 658 owner: alice.address().to_string(), 659 }], 660 outputs: vec![TxOutput { 661 address: bob.address().to_string(), 662 amount: 1, 663 }], 664 fee: 0, 665 }; 666 let transaction = unsigned.sign(&alice); 667 668 let error = ledger.submit_transaction(transaction).unwrap_err(); 669 670 assert!(format!("{error:#}").contains("invalid input outpoint txid")); 671 assert!(ledger.pending().is_empty()); 672 } 673 674 #[test] 675 fn missing_input_transaction_goes_to_orphan_pool_not_pending_mempool() { 676 let alice = Wallet::from_seed("missing-input-orphan-alice"); 677 let bob = Wallet::from_seed("missing-input-orphan-bob"); 678 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 679 let transaction = UnsignedUtxoTransaction::Transfer { 680 inputs: vec![UnsignedTxInput { 681 outpoint: OutPoint { 682 txid: hex_hash("missing-input-orphan"), 683 index: 0, 684 }, 685 owner: alice.address().to_string(), 686 }], 687 outputs: vec![TxOutput { 688 address: bob.address().to_string(), 689 amount: 1, 690 }], 691 fee: 0, 692 } 693 .sign(&alice); 694 695 let outcome = ledger.submit_transaction_with_outcome(transaction).unwrap(); 696 697 assert_eq!(outcome, TransactionSubmitOutcome::Added); 698 assert!(ledger.pending().is_empty()); 699 assert_eq!(ledger.orphan_transactions().len(), 1); 700 } 701 702 #[test] 703 fn vdf_retarget_observed_block_time_is_clamped() { 704 assert_eq!( 705 clamped_vdf_retarget_observed_block_ms(1), 706 MIN_VDF_RETARGET_OBSERVED_BLOCK_MS 707 ); 708 assert_eq!( 709 clamped_vdf_retarget_observed_block_ms(VDF_TARGET_BLOCK_MS), 710 VDF_TARGET_BLOCK_MS 711 ); 712 assert_eq!( 713 clamped_vdf_retarget_observed_block_ms(u64::MAX), 714 MAX_VDF_RETARGET_OBSERVED_BLOCK_MS 715 ); 716 } 717 718 #[test] 719 fn vdf_retarget_observed_block_time_ignores_ticket_fallback_ranks() { 720 let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); 721 let fallback_child = 722 vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 2, FinalizerMode::Ticket, 1); 723 724 assert_eq!( 725 vdf_retarget_observed_block_ms(&parent, &fallback_child), 726 None 727 ); 728 } 729 730 #[test] 731 fn vdf_retarget_observed_block_time_ignores_recovery_blocks() { 732 let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); 733 let recovery_child = 734 vdf_retarget_sample_block(RECOVERY_BLOCK_DELAY_MS, FinalizerMode::Recovery, 0); 735 736 assert_eq!( 737 vdf_retarget_observed_block_ms(&parent, &recovery_child), 738 None 739 ); 740 } 741 742 #[test] 743 fn vdf_retarget_keeps_rounds_inside_deadband() { 744 let current = 1_000; 745 let low_deadband_edge = 746 VDF_TARGET_BLOCK_MS - VDF_TARGET_BLOCK_MS * VDF_RETARGET_DEADBAND_PERCENT as u64 / 100; 747 let high_deadband_edge = 748 VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS * VDF_RETARGET_DEADBAND_PERCENT as u64 / 100; 749 750 assert_eq!(retarget_vdf_rounds(current, low_deadband_edge), current); 751 assert_eq!(retarget_vdf_rounds(current, VDF_TARGET_BLOCK_MS), current); 752 assert_eq!(retarget_vdf_rounds(current, high_deadband_edge), current); 753 } 754 755 #[test] 756 fn vdf_retarget_limits_each_step_to_two_percent() { 757 let current = 1_000; 758 759 assert_eq!( 760 retarget_vdf_rounds(current, MIN_VDF_RETARGET_OBSERVED_BLOCK_MS), 761 1_020 762 ); 763 assert_eq!( 764 retarget_vdf_rounds(current, MAX_VDF_RETARGET_OBSERVED_BLOCK_MS), 765 980 766 ); 767 } 768 769 #[test] 770 fn vdf_rounds_retarget_below_legacy_u32_limit_after_slow_blocks() { 771 let wallet = Wallet::from_seed("vdf-rounds-slow-above-u32"); 772 let initial_rounds = u64::from(u32::MAX); 773 let mut allocations = BTreeMap::new(); 774 allocations.insert(wallet.address().to_string(), 1_000); 775 let mut ledger = Ledger::new_with_genesis_burns( 776 allocations, 777 vec![GenesisBurn::new(wallet.address(), 1)], 778 initial_rounds, 779 ) 780 .unwrap(); 781 782 let block1 = apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); 783 assert_eq!(block1.vdf_rounds, initial_rounds); 784 assert_eq!(ledger.vdf_rounds(), initial_rounds); 785 786 let block2 = apply_preverified_burn_block_at( 787 &mut ledger, 788 &wallet, 789 VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS * 2, 790 ); 791 assert_eq!(block2.vdf_rounds, initial_rounds); 792 793 assert!( 794 ledger.vdf_rounds() < initial_rounds, 795 "slow blocks should retarget below the legacy u32 VDF rounds ceiling" 796 ); 797 } 798 799 #[test] 800 fn fallback_block_is_excluded_from_vdf_retarget_observations() { 801 let alice = Wallet::from_seed("fallback-retarget-alice"); 802 let bob = Wallet::from_seed("fallback-retarget-bob"); 803 let wallets = [&alice, &bob]; 804 let mut genesis = BTreeMap::new(); 805 genesis.insert(alice.address().to_string(), 1_000); 806 genesis.insert(bob.address().to_string(), 1_000); 807 let mut ledger = Ledger::new_with_genesis_burns( 808 genesis, 809 vec![ 810 GenesisBurn::new(alice.address(), 1), 811 GenesisBurn::new(bob.address(), 1), 812 ], 813 100, 814 ) 815 .unwrap(); 816 817 let primary = ledger.expected_leader_for_next_block().unwrap(); 818 let primary_wallet = wallets 819 .into_iter() 820 .find(|wallet| wallet.address() == primary) 821 .unwrap(); 822 apply_preverified_burn_block_at(&mut ledger, primary_wallet, VDF_TARGET_BLOCK_MS); 823 assert_eq!(ledger.vdf_rounds(), 100); 824 825 let primary = ledger.expected_leader_for_next_block().unwrap(); 826 let fallback = wallets 827 .into_iter() 828 .find(|wallet| wallet.address() != primary) 829 .unwrap(); 830 let timestamp_ms = ledger.tip().timestamp_ms + 1; 831 let block = apply_preverified_burn_block_at(&mut ledger, fallback, timestamp_ms); 832 833 assert_eq!(block.finalizer_rank, 1); 834 assert_eq!(block.vdf_rounds, 200); 835 assert_eq!(ledger.vdf_rounds(), 100); 836 } 837 838 #[test] 839 fn recovery_block_is_excluded_from_vdf_retarget_observations() { 840 let alice = Wallet::from_seed("recovery-retarget-alice"); 841 let bob = Wallet::from_seed("recovery-retarget-bob"); 842 let mut genesis = BTreeMap::new(); 843 genesis.insert(alice.address().to_string(), 1_000); 844 genesis.insert(bob.address().to_string(), 1_000); 845 let mut ledger = 846 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 100) 847 .unwrap(); 848 849 apply_preverified_burn_block_at(&mut ledger, &alice, VDF_TARGET_BLOCK_MS); 850 assert_eq!(ledger.vdf_rounds(), 100); 851 852 let burn = ledger.build_burn(&bob, 1, 0).unwrap(); 853 ledger.submit_transaction(burn).unwrap(); 854 let block = ledger 855 .mine_recovery_block(&bob, VDF_TARGET_BLOCK_MS + RECOVERY_BLOCK_DELAY_MS) 856 .unwrap(); 857 assert_eq!(block.finalizer_mode, FinalizerMode::Recovery); 858 ledger.apply_block(block).unwrap(); 859 860 assert_eq!(ledger.vdf_rounds(), 100); 861 } 862 863 #[test] 864 fn generated_vdf_retarget_decreases_after_slow_blocks_above_legacy_limit() { 865 let legacy_limit = u64::from(u32::MAX); 866 let slow_observed_ms = [ 867 VDF_TARGET_BLOCK_MS * 6 / 5, 868 VDF_TARGET_BLOCK_MS * 2, 869 VDF_TARGET_BLOCK_MS * 3, 870 MAX_VDF_RETARGET_OBSERVED_BLOCK_MS, 871 ]; 872 873 for seed in 0..16_u64 { 874 let wallet = Wallet::from_seed(&format!("generated-vdf-retarget-{seed}")); 875 let initial_rounds = legacy_limit + 1 + seed * 1_000_003; 876 let mut allocations = BTreeMap::new(); 877 allocations.insert(wallet.address().to_string(), 1_000); 878 let mut ledger = Ledger::new_with_genesis_burns( 879 allocations, 880 vec![GenesisBurn::new(wallet.address(), 1)], 881 initial_rounds, 882 ) 883 .unwrap(); 884 let observed_ms = slow_observed_ms[seed as usize % slow_observed_ms.len()]; 885 886 apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); 887 let second = apply_preverified_burn_block_at( 888 &mut ledger, 889 &wallet, 890 VDF_TARGET_BLOCK_MS + observed_ms, 891 ); 892 893 assert_eq!(second.vdf_rounds, initial_rounds); 894 assert!( 895 ledger.vdf_rounds() < initial_rounds, 896 "seed {seed} with observed {observed_ms}ms should lower VDF rounds from {initial_rounds}, got {}", 897 ledger.vdf_rounds() 898 ); 899 900 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 901 ledger.submit_transaction(burn).unwrap(); 902 let next_work = ledger 903 .prepare_next_block(wallet.address(), second.timestamp_ms + VDF_TARGET_BLOCK_MS) 904 .unwrap(); 905 assert_eq!(next_work.vdf_rounds(), ledger.vdf_rounds()); 906 } 907 } 908 909 #[test] 910 fn block_timestamp_future_check_uses_supplied_network_time() { 911 let wallet = Wallet::from_seed("adjusted-time-domain"); 912 let mut allocations = BTreeMap::new(); 913 allocations.insert(wallet.address().to_string(), 1_000); 914 let mut ledger = Ledger::new(allocations, 1); 915 916 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 917 assert!(ledger.submit_transaction(burn).unwrap()); 918 let block = ledger.mine_next_block(&wallet, 10 * 60 * 1_000).unwrap(); 919 920 let error = ledger.apply_block_at(block, 1_000).unwrap_err(); 921 922 assert!(format!("{error:#}").contains("too far in the future")); 923 } 924 925 #[test] 926 fn ticket_block_timestamp_uses_finalizer_rank_time_slot() { 927 let alice = Wallet::from_seed("rank-slot-alice"); 928 let bob = Wallet::from_seed("rank-slot-bob"); 929 let wallets = [alice.clone(), bob.clone()]; 930 let mut allocations = BTreeMap::new(); 931 allocations.insert(alice.address().to_string(), 1_000); 932 allocations.insert(bob.address().to_string(), 1_000); 933 let mut ledger = Ledger::new_with_genesis_burns( 934 allocations, 935 vec![ 936 GenesisBurn::new(alice.address(), 1), 937 GenesisBurn::new(bob.address(), 1), 938 ], 939 100, 940 ) 941 .unwrap(); 942 943 let primary = wallet_for_address(&wallets, &ledger.expected_leader_for_next_block().unwrap()); 944 let burn = ledger.build_burn(primary, 1, 0).unwrap(); 945 ledger.submit_transaction(burn).unwrap(); 946 let work = ledger.prepare_next_block(primary.address(), 1).unwrap(); 947 assert_eq!(work.timestamp_ms(), 1); 948 let block = work.finish(primary, "preverified-vdf".to_string()); 949 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 950 951 let fallback = wallets 952 .iter() 953 .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1)) 954 .expect("expected rank 1 fallback"); 955 let burn = ledger.build_burn(fallback, 1, 0).unwrap(); 956 ledger.submit_transaction(burn).unwrap(); 957 let parent_timestamp = ledger.tip().timestamp_ms; 958 let work = ledger 959 .prepare_next_block(fallback.address(), parent_timestamp + 1) 960 .unwrap(); 961 962 assert_eq!( 963 work.timestamp_ms(), 964 parent_timestamp + VDF_TARGET_BLOCK_MS * 2 965 ); 966 assert_eq!(work.vdf_rounds(), ledger.vdf_rounds() * 2); 967 } 968 969 #[test] 970 fn required_anchor_burn_is_selected_before_higher_fee_burns_when_block_is_full() { 971 let wallet = Wallet::from_seed("required-anchor-priority-wallet"); 972 let mut allocations = BTreeMap::new(); 973 allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA); 974 let mut ledger = Ledger::new_with_genesis_burns( 975 allocations, 976 vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], 977 1, 978 ) 979 .unwrap(); 980 let high_fee_outpoint = named_test_outpoint("required-anchor-priority-high-fee"); 981 let anchor_outpoint = named_test_outpoint("required-anchor-priority-anchor"); 982 ledger.utxos.insert( 983 high_fee_outpoint.clone(), 984 TxOutput { 985 address: wallet.address().to_string(), 986 amount: 3, 987 }, 988 ); 989 ledger.utxos.insert( 990 anchor_outpoint.clone(), 991 TxOutput { 992 address: wallet.address().to_string(), 993 amount: 2, 994 }, 995 ); 996 let high_fee_burn = ledger 997 .build_burn_with_inputs(&wallet, 1, 1, &[high_fee_outpoint]) 998 .unwrap(); 999 let anchor_burn = ledger 1000 .build_burn_with_inputs(&wallet, 1, 0, &[anchor_outpoint]) 1001 .unwrap(); 1002 ledger.submit_transaction(high_fee_burn).unwrap(); 1003 ledger.submit_transaction(anchor_burn.clone()).unwrap(); 1004 ledger.launch_profile.max_block_transactions = 1; 1005 1006 let work = ledger 1007 .prepare_next_block_with_required_burn_and_reveal_bundles( 1008 wallet.address(), 1009 1, 1010 Vec::new(), 1011 Some(anchor_burn.signature()), 1012 ) 1013 .unwrap(); 1014 let block = work.finish(&wallet, "preverified-vdf".to_string()); 1015 1016 assert_eq!(block.transactions.len(), 1); 1017 assert_eq!(block.transactions[0].signature(), anchor_burn.signature()); 1018 } 1019 1020 #[test] 1021 fn late_ticket_vdf_completion_is_visible_to_retarget() { 1022 let wallet = Wallet::from_seed("late-ticket-vdf-wallet"); 1023 let mut allocations = BTreeMap::new(); 1024 allocations.insert(wallet.address().to_string(), 1_000); 1025 let mut ledger = Ledger::new_with_genesis_burns( 1026 allocations, 1027 vec![GenesisBurn::new(wallet.address(), 1)], 1028 100, 1029 ) 1030 .unwrap(); 1031 1032 apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); 1033 assert_eq!(ledger.vdf_rounds(), 100); 1034 1035 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 1036 ledger.submit_transaction(burn).unwrap(); 1037 let work = ledger 1038 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 1039 .unwrap(); 1040 let scheduled_timestamp = work.timestamp_ms(); 1041 let late_timestamp = ledger.tip().timestamp_ms + VDF_TARGET_BLOCK_MS * 3; 1042 assert!(late_timestamp > scheduled_timestamp); 1043 1044 let block = work.finish_at(&wallet, "preverified-vdf".to_string(), late_timestamp); 1045 assert_eq!(block.timestamp_ms, late_timestamp); 1046 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 1047 1048 assert!( 1049 ledger.vdf_rounds() < 100, 1050 "late VDF completion should lower future VDF rounds" 1051 ); 1052 } 1053 1054 #[test] 1055 fn block_before_finalizer_rank_time_slot_is_rejected() { 1056 let alice = Wallet::from_seed("rank-slot-reject-alice"); 1057 let bob = Wallet::from_seed("rank-slot-reject-bob"); 1058 let wallets = [alice.clone(), bob.clone()]; 1059 let mut allocations = BTreeMap::new(); 1060 allocations.insert(alice.address().to_string(), 1_000); 1061 allocations.insert(bob.address().to_string(), 1_000); 1062 let mut ledger = Ledger::new_with_genesis_burns( 1063 allocations, 1064 vec![ 1065 GenesisBurn::new(alice.address(), 1), 1066 GenesisBurn::new(bob.address(), 1), 1067 ], 1068 100, 1069 ) 1070 .unwrap(); 1071 1072 let primary = wallet_for_address(&wallets, &ledger.expected_leader_for_next_block().unwrap()); 1073 let burn = ledger.build_burn(primary, 1, 0).unwrap(); 1074 ledger.submit_transaction(burn).unwrap(); 1075 let work = ledger.prepare_next_block(primary.address(), 1).unwrap(); 1076 let block = work.finish(primary, "preverified-vdf".to_string()); 1077 ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); 1078 1079 let fallback = wallets 1080 .iter() 1081 .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1)) 1082 .expect("expected rank 1 fallback"); 1083 let burn = ledger.build_burn(fallback, 1, 0).unwrap(); 1084 ledger.submit_transaction(burn).unwrap(); 1085 let parent_timestamp = ledger.tip().timestamp_ms; 1086 let work = ledger 1087 .prepare_next_block(fallback.address(), parent_timestamp + 1) 1088 .unwrap(); 1089 let mut block = work.finish(fallback, "preverified-vdf".to_string()); 1090 block.timestamp_ms = parent_timestamp + VDF_TARGET_BLOCK_MS * 2 - 1; 1091 block.hash = block.compute_hash(); 1092 1093 let error = ledger 1094 .apply_preverified_block_at(block, u64::MAX) 1095 .unwrap_err(); 1096 1097 assert!(format!("{error:#}").contains("before finalizer rank 1 time slot")); 1098 } 1099 1100 #[test] 1101 fn miner_skips_oversized_pending_transaction_and_keeps_fitting_fee_transaction() { 1102 let alice = Wallet::from_seed("oversized-select-alice"); 1103 let bob = Wallet::from_seed("oversized-select-bob"); 1104 let carol = Wallet::from_seed("oversized-select-carol"); 1105 let mut allocations = BTreeMap::new(); 1106 allocations.insert(alice.address().to_string(), 1); 1107 allocations.insert(bob.address().to_string(), 300_000); 1108 allocations.insert(carol.address().to_string(), 300_000); 1109 let mut ledger = 1110 Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10) 1111 .unwrap(); 1112 let burn = ledger.build_burn(&alice, 1, 0).unwrap(); 1113 ledger.submit_transaction(burn).unwrap(); 1114 let oversized = 1115 transfer_with_extra_zero_outputs(&ledger, &bob, alice.address(), 1, 100_000, 4_000); 1116 let fitting = ledger 1117 .build_transfer(&carol, alice.address(), 1, 5) 1118 .unwrap(); 1119 assert!(oversized.serialized_size_bytes().unwrap() > MAX_BLOCK_BYTES); 1120 ledger.submit_transaction(oversized.clone()).unwrap(); 1121 ledger.submit_transaction(fitting.clone()).unwrap(); 1122 1123 let block = ledger.mine_next_block(&alice, 1).unwrap(); 1124 let signatures = block 1125 .transactions 1126 .iter() 1127 .map(|tx| tx.signature().to_string()) 1128 .collect::<Vec<_>>(); 1129 1130 assert!(!signatures.contains(&oversized.signature().to_string())); 1131 assert!(signatures.contains(&fitting.signature().to_string())); 1132 assert!(block.serialized_size_bytes().unwrap() <= MAX_BLOCK_BYTES); 1133 } 1134 1135 #[test] 1136 fn transfer_can_spend_selected_utxos_when_they_cover_amount_and_fee() { 1137 let alice = Wallet::from_seed("selected-utxos-alice"); 1138 let bob = Wallet::from_seed("selected-utxos-bob"); 1139 let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); 1140 let selected = vec![test_utxo_outpoint(2)]; 1141 1142 let tx = ledger 1143 .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) 1144 .unwrap(); 1145 1146 let Transaction::Transfer { 1147 inputs, outputs, .. 1148 } = &tx 1149 else { 1150 panic!("expected transfer"); 1151 }; 1152 assert_eq!(inputs.len(), 1); 1153 assert_eq!(inputs[0].outpoint, selected[0]); 1154 assert_eq!( 1155 outputs, 1156 &[ 1157 TxOutput { 1158 address: bob.address().to_string(), 1159 amount: 2 1160 }, 1161 TxOutput { 1162 address: alice.address().to_string(), 1163 amount: 2 1164 } 1165 ] 1166 ); 1167 1168 ledger.submit_transaction(tx).unwrap(); 1169 let balances = pending_balances(&ledger); 1170 assert_eq!(balances.get(bob.address()).copied(), Some(2)); 1171 assert_eq!(balances.get(alice.address()).copied(), Some(7)); 1172 } 1173 1174 #[test] 1175 fn burn_can_spend_selected_utxos_when_they_cover_amount_and_fee() { 1176 let alice = Wallet::from_seed("selected-burn-utxos-alice"); 1177 let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); 1178 let selected = vec![test_utxo_outpoint(1)]; 1179 1180 let tx = ledger 1181 .build_burn_with_inputs(&alice, 1, 1, &selected) 1182 .unwrap(); 1183 1184 let Transaction::Burn { 1185 inputs, 1186 change, 1187 amount, 1188 fee, 1189 .. 1190 } = &tx 1191 else { 1192 panic!("expected burn"); 1193 }; 1194 assert_eq!(*amount, 1); 1195 assert_eq!(*fee, 1); 1196 assert_eq!(inputs.len(), 1); 1197 assert_eq!(inputs[0].outpoint, selected[0]); 1198 assert_eq!( 1199 change, 1200 &[TxOutput { 1201 address: alice.address().to_string(), 1202 amount: 1 1203 }] 1204 ); 1205 1206 ledger.submit_transaction(tx).unwrap(); 1207 let balances = pending_balances(&ledger); 1208 assert_eq!(balances.get(alice.address()).copied(), Some(8)); 1209 } 1210 1211 #[test] 1212 fn transfer_rejects_selected_utxos_that_do_not_cover_amount_plus_fee() { 1213 let alice = Wallet::from_seed("selected-utxos-insufficient-alice"); 1214 let bob = Wallet::from_seed("selected-utxos-insufficient-bob"); 1215 let ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); 1216 let selected = vec![test_utxo_outpoint(0)]; 1217 1218 let error = ledger 1219 .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) 1220 .unwrap_err(); 1221 1222 assert!(format!("{error:#}").contains("selected UTXOs do not cover")); 1223 } 1224 1225 #[test] 1226 fn transfer_rejects_selected_utxos_owned_by_someone_else() { 1227 let alice = Wallet::from_seed("selected-utxos-owner-alice"); 1228 let bob = Wallet::from_seed("selected-utxos-owner-bob"); 1229 let carol = Wallet::from_seed("selected-utxos-owner-carol"); 1230 let mut ledger = ledger_with_wallet_utxos(&alice, &[5]); 1231 ledger.utxos.insert( 1232 named_test_outpoint("carol"), 1233 TxOutput { 1234 address: carol.address().to_string(), 1235 amount: 5, 1236 }, 1237 ); 1238 let selected = vec![named_test_outpoint("carol")]; 1239 1240 let error = ledger 1241 .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) 1242 .unwrap_err(); 1243 1244 assert!(format!("{error:#}").contains("is not owned")); 1245 } 1246 1247 #[test] 1248 fn transfer_rejects_when_combined_utxos_do_not_cover_amount_plus_fee() { 1249 let alice = Wallet::from_seed("combine-insufficient-alice"); 1250 let bob = Wallet::from_seed("combine-insufficient-bob"); 1251 let ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 1]); 1252 1253 let error = ledger 1254 .build_transfer(&alice, bob.address(), 3, 1) 1255 .unwrap_err(); 1256 1257 assert!(format!("{error:#}").contains("insufficient funds")); 1258 } 1259 1260 #[test] 1261 fn pending_change_from_combined_utxos_can_fund_next_transaction() { 1262 let alice = Wallet::from_seed("combine-pending-change-alice"); 1263 let bob = Wallet::from_seed("combine-pending-change-bob"); 1264 let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); 1265 1266 let first = ledger.build_transfer(&alice, bob.address(), 3, 0).unwrap(); 1267 let first_signature = first.signature().to_string(); 1268 ledger.submit_transaction(first).unwrap(); 1269 1270 let second = ledger.build_transfer(&alice, bob.address(), 1, 0).unwrap(); 1271 let Transaction::Transfer { inputs, .. } = &second else { 1272 panic!("expected transfer"); 1273 }; 1274 assert_eq!(inputs.len(), 1); 1275 assert_eq!(inputs[0].outpoint.txid, first_signature); 1276 assert_eq!(inputs[0].outpoint.index, 1); 1277 1278 ledger.submit_transaction(second).unwrap(); 1279 let balances = pending_balances(&ledger); 1280 assert_eq!( 1281 balances.get(alice.address()).copied().unwrap_or_default(), 1282 0 1283 ); 1284 assert_eq!(balances.get(bob.address()).copied(), Some(4)); 1285 } 1286 1287 #[test] 1288 fn winning_burn_ticket_is_consumed_even_when_window_remains() { 1289 let mut tickets = vec![ 1290 BurnTicket { 1291 id: "high-burn".to_string(), 1292 owner: "alice".to_string(), 1293 amount: 10_000, 1294 eligible_from_height: 4, 1295 eligible_until_height: 6, 1296 }, 1297 BurnTicket { 1298 id: "small-burn".to_string(), 1299 owner: "bob".to_string(), 1300 amount: 1, 1301 eligible_from_height: 5, 1302 eligible_until_height: 7, 1303 }, 1304 ]; 1305 let mut block = Block { 1306 height: 4, 1307 prev_hash: "0".repeat(64), 1308 timestamp_ms: 1, 1309 miner: "alice".to_string(), 1310 finalizer_mode: FinalizerMode::Ticket, 1311 finalizer_rank: 0, 1312 reward: BLOCK_REWARD, 1313 vdf_rounds: 1, 1314 vdf_output: "vdf".to_string(), 1315 leader_proof: Some(LeaderProof { 1316 ticket_id: "high-burn".to_string(), 1317 public_key: "alice".to_string(), 1318 signature: "signature".to_string(), 1319 }), 1320 blinded_transactions: Vec::new(), 1321 reveal_bundle_section: RevealBundleSection::default(), 1322 transactions: Vec::new(), 1323 hash: String::new(), 1324 }; 1325 block.hash = block.compute_hash(); 1326 1327 consume_leader_ticket(&block, &mut tickets).unwrap(); 1328 1329 assert!( 1330 tickets.iter().all(|ticket| ticket.id != "high-burn"), 1331 "a winning burn must not remain eligible for the rest of its window" 1332 ); 1333 assert!( 1334 tickets.iter().any(|ticket| ticket.id == "small-burn"), 1335 "unselected future tickets should remain pending" 1336 ); 1337 } 1338 1339 #[test] 1340 fn burn_leader_ranks_for_block_reconstructs_historical_ticket_order() { 1341 let alice = Wallet::from_seed("burn-rank-alice"); 1342 let bob = Wallet::from_seed("burn-rank-bob"); 1343 let mut allocations = BTreeMap::new(); 1344 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 1345 allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); 1346 let ledger = Ledger::new_with_genesis_burns( 1347 allocations, 1348 vec![ 1349 GenesisBurn::new(alice.address(), MICRO_IUNA), 1350 GenesisBurn::new(bob.address(), MICRO_IUNA), 1351 ], 1352 1, 1353 ) 1354 .unwrap(); 1355 1356 let ranks = ledger.burn_leader_ranks_for_block(1).unwrap(); 1357 let leader = ledger.expected_leader_for_next_block().unwrap(); 1358 1359 assert_eq!(ranks.len(), 2); 1360 assert_eq!(ranks[0].rank, 0); 1361 assert_eq!(ranks[0].owner, leader); 1362 assert!(ranks.iter().all(|rank| rank.amount == MICRO_IUNA)); 1363 assert_eq!(ledger.burn_leader_ranks_for_block(0).unwrap(), Vec::new()); 1364 1365 let batch = ledger.burn_leader_ranks_for_blocks([0, 1]).unwrap(); 1366 assert!(batch.get(&0).unwrap().is_empty()); 1367 assert_eq!(batch.get(&1), Some(&ranks)); 1368 } 1369 1370 #[test] 1371 fn mine_recipient_is_bound_to_proof_hash() { 1372 let alice = Wallet::from_seed("mine-proof-alice"); 1373 let bob = Wallet::from_seed("mine-proof-bob"); 1374 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1375 let mut forged = unsigned_mine(&ledger, alice.address()); 1376 if let Transaction::Mine { recipient, .. } = &mut forged { 1377 *recipient = bob.address().to_string(); 1378 } 1379 1380 let error = ledger.submit_transaction(forged).unwrap_err(); 1381 1382 assert!(format!("{error:#}").contains("proof hash is invalid")); 1383 } 1384 1385 #[test] 1386 fn mine_action_uses_fixed_reward_and_fixed_finalizer_fee() { 1387 let alice = Wallet::from_seed("mine-fixed-reward-alice"); 1388 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1389 1390 let mine = ledger.build_mine(alice.address()).unwrap(); 1391 1392 assert_eq!(mine.amount(), MINE_REWARD); 1393 assert_eq!(mine.fee(), MINE_FINALIZER_FEE); 1394 assert!(ledger.submit_transaction(mine).unwrap()); 1395 } 1396 1397 #[test] 1398 fn burn_fee_goes_to_block_finalizer() { 1399 let alice = Wallet::from_seed("burn-fee-finalizer-alice"); 1400 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1401 1402 let burn_fee = 12; 1403 let burn = ledger 1404 .build_burn(&alice, TEST_BURN_AMOUNT, burn_fee) 1405 .unwrap(); 1406 ledger.submit_transaction(burn).unwrap(); 1407 let prepared = ledger.prepare_next_block(alice.address(), 1).unwrap(); 1408 1409 assert_eq!(prepared.reward, burn_fee); 1410 } 1411 1412 #[test] 1413 fn blinded_burn_commits_ciphertext_and_reveal_executes_later() { 1414 let alice = Wallet::from_seed("blinded-burn-finalizer-alice"); 1415 let bob = Wallet::from_seed("blinded-burn-finalizer-bob"); 1416 let carol = Wallet::from_seed("blinded-burn-carol"); 1417 let finalizers = [alice.clone(), bob.clone()]; 1418 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1419 let fee = 100; 1420 let burn_amount = 3; 1421 let before_carol = ledger.balance_of(carol.address()); 1422 1423 let blinded = ledger 1424 .build_blinded_burn(&carol, burn_amount, fee, ledger.height() + 4) 1425 .unwrap(); 1426 assert!(!blinded.transaction.ciphertext.contains("burn")); 1427 assert!(!blinded.transaction.ciphertext.contains(carol.address())); 1428 ledger 1429 .submit_blinded_transaction(blinded.transaction.clone()) 1430 .unwrap(); 1431 queue_next_leader_burn(&mut ledger, &finalizers); 1432 1433 let commit_block = mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1434 let inclusion_finalizer = commit_block.miner.clone(); 1435 assert_eq!( 1436 commit_block 1437 .transactions 1438 .iter() 1439 .filter(|transaction| transaction.is_burn()) 1440 .count(), 1441 1 1442 ); 1443 assert_eq!(commit_block.blinded_transactions, vec![blinded.transaction]); 1444 assert_eq!(commit_block.reward, 0); 1445 let before_inclusion_finalizer = ledger.balance_of(&inclusion_finalizer); 1446 1447 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1448 queue_next_leader_burn(&mut ledger, &finalizers); 1449 let reveal_block = 1450 mine_preverified_as_next_leader_with_reveal_bundles(&mut ledger, &finalizers, 2); 1451 let reveal_executor = reveal_block.miner.clone(); 1452 1453 assert_eq!(reveal_block.all_blinded_reveals().len(), 1); 1454 assert_eq!( 1455 ledger.balance_of(carol.address()), 1456 before_carol - burn_amount - fee 1457 ); 1458 assert!(ledger.tickets.iter().any(|ticket| { 1459 ticket.owner == carol.address() 1460 && ticket.amount == burn_amount 1461 && ticket.eligible_from_height 1462 == reveal_block.height + ledger.launch_profile.ticket_maturity_delay_heights 1463 })); 1464 let reveal_plaintext_burn_spent_by_inclusion_finalizer = reveal_block 1465 .transactions 1466 .iter() 1467 .filter(|transaction| { 1468 transaction.is_burn() && transaction.sender() == inclusion_finalizer.as_str() 1469 }) 1470 .fold(0_u64, |total, transaction| { 1471 total + transaction.amount() + transaction.fee() 1472 }); 1473 let committer_fee = blinded_fee_share(fee, BLINDED_COMMITTER_FEE_BPS); 1474 let reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1475 fee, 1476 reveal_block.included_reveal_bundle_count(), 1477 ledger 1478 .burn_leader_ranks_for_block(reveal_block.height) 1479 .unwrap() 1480 .len(), 1481 ); 1482 let reveal_bundle_signer_fee = blinded_fee_share(fee, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS); 1483 let commitment = &commit_block.blinded_transactions[0].commitment; 1484 assert_eq!( 1485 ledger 1486 .utxos 1487 .get(&blinded_committer_fee_outpoint(commitment)) 1488 .unwrap(), 1489 &TxOutput { 1490 address: inclusion_finalizer.clone(), 1491 amount: committer_fee, 1492 } 1493 ); 1494 assert!( 1495 !ledger 1496 .utxos 1497 .contains_key(&blinded_executor_fee_outpoint(commitment)) 1498 ); 1499 assert_eq!(reveal_block.reward, reveal_finalizer_fee); 1500 assert_eq!( 1501 ledger 1502 .utxos 1503 .get(&reward_outpoint(&reveal_block.hash)) 1504 .unwrap(), 1505 &TxOutput { 1506 address: reveal_executor.clone(), 1507 amount: reveal_finalizer_fee, 1508 } 1509 ); 1510 for signature in &reveal_block.reveal_bundle_section.signatures { 1511 assert_eq!( 1512 ledger 1513 .utxos 1514 .get(&blinded_reveal_bundle_signer_fee_outpoint( 1515 commitment, 1516 signature.slot 1517 )) 1518 .unwrap(), 1519 &TxOutput { 1520 address: signature.member.clone(), 1521 amount: reveal_bundle_signer_fee, 1522 } 1523 ); 1524 } 1525 let mut inclusion_finalizer_fee = committer_fee; 1526 if inclusion_finalizer == reveal_executor { 1527 inclusion_finalizer_fee += reveal_finalizer_fee; 1528 } 1529 inclusion_finalizer_fee += reveal_block 1530 .reveal_bundle_section 1531 .signatures 1532 .iter() 1533 .filter(|signature| signature.member == inclusion_finalizer) 1534 .count() as u64 1535 * reveal_bundle_signer_fee; 1536 assert_eq!( 1537 ledger.balance_of(&inclusion_finalizer), 1538 before_inclusion_finalizer + inclusion_finalizer_fee 1539 - reveal_plaintext_burn_spent_by_inclusion_finalizer 1540 ); 1541 } 1542 1543 #[test] 1544 fn blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { 1545 let alice = Wallet::from_seed("aggregated-finalizer-fee-alice"); 1546 let bob = Wallet::from_seed("aggregated-finalizer-fee-bob"); 1547 let carol = Wallet::from_seed("aggregated-finalizer-fee-carol"); 1548 let dave = Wallet::from_seed("aggregated-finalizer-fee-dave"); 1549 let finalizers = [alice.clone(), bob.clone()]; 1550 let mut ledger = ledger_with_finalizers( 1551 &finalizers, 1552 &[(&carol, 10 * MICRO_IUNA), (&dave, 10 * MICRO_IUNA)], 1553 ); 1554 let first_fee = 100; 1555 let second_fee = 200; 1556 let first_blinded = ledger 1557 .build_blinded_burn(&carol, 3, first_fee, ledger.height() + 4) 1558 .unwrap(); 1559 let second_blinded = ledger 1560 .build_blinded_burn(&dave, 4, second_fee, ledger.height() + 4) 1561 .unwrap(); 1562 let first_commitment = first_blinded.transaction.commitment.clone(); 1563 let second_commitment = second_blinded.transaction.commitment.clone(); 1564 ledger 1565 .submit_blinded_transaction(first_blinded.transaction) 1566 .unwrap(); 1567 ledger 1568 .submit_blinded_transaction(second_blinded.transaction) 1569 .unwrap(); 1570 queue_next_leader_burn(&mut ledger, &finalizers); 1571 let commit_timestamp_ms = ledger 1572 .tip() 1573 .timestamp_ms 1574 .saturating_add(VDF_TARGET_BLOCK_MS); 1575 let commit_block = 1576 mine_preverified_as_next_leader(&mut ledger, &finalizers, commit_timestamp_ms); 1577 assert_eq!(commit_block.height, 1); 1578 1579 ledger.submit_blinded_reveal(first_blinded.reveal).unwrap(); 1580 ledger.submit_blinded_reveal(second_blinded.reveal).unwrap(); 1581 queue_next_leader_burn(&mut ledger, &finalizers); 1582 let reveal_timestamp_ms = ledger 1583 .tip() 1584 .timestamp_ms 1585 .saturating_add(VDF_TARGET_BLOCK_MS); 1586 let reveal_block = prepare_preverified_as_next_leader_with_reveal_bundles( 1587 &ledger, 1588 &finalizers, 1589 reveal_timestamp_ms, 1590 ); 1591 let first_reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1592 first_fee, 1593 reveal_block.included_reveal_bundle_count(), 1594 ledger 1595 .burn_leader_ranks_for_block(reveal_block.height) 1596 .unwrap() 1597 .len(), 1598 ); 1599 let second_reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1600 second_fee, 1601 reveal_block.included_reveal_bundle_count(), 1602 ledger 1603 .burn_leader_ranks_for_block(reveal_block.height) 1604 .unwrap() 1605 .len(), 1606 ); 1607 let aggregate_reveal_finalizer_fee = first_reveal_finalizer_fee 1608 .checked_add(second_reveal_finalizer_fee) 1609 .unwrap(); 1610 1611 assert_eq!(reveal_block.height, 2); 1612 assert_eq!(reveal_block.reward, aggregate_reveal_finalizer_fee); 1613 let mut plain_fee_reward_block = reveal_block.clone(); 1614 plain_fee_reward_block.reward = fee_reward(&plain_fee_reward_block.transactions).unwrap(); 1615 plain_fee_reward_block.hash = plain_fee_reward_block.compute_hash(); 1616 let error = ledger 1617 .clone() 1618 .apply_preverified_block_at(plain_fee_reward_block, u64::MAX) 1619 .unwrap_err(); 1620 assert!(format!("{error:#}").contains("block reward is invalid")); 1621 1622 ledger 1623 .apply_preverified_block_at(reveal_block.clone(), u64::MAX) 1624 .unwrap(); 1625 assert!( 1626 !ledger 1627 .utxos 1628 .contains_key(&blinded_executor_fee_outpoint(&first_commitment)) 1629 ); 1630 assert!( 1631 !ledger 1632 .utxos 1633 .contains_key(&blinded_executor_fee_outpoint(&second_commitment)) 1634 ); 1635 assert_eq!( 1636 ledger.utxos.get(&reward_outpoint(&reveal_block.hash)), 1637 Some(&TxOutput { 1638 address: reveal_block.miner.clone(), 1639 amount: aggregate_reveal_finalizer_fee, 1640 }) 1641 ); 1642 } 1643 1644 #[test] 1645 fn blinded_utxo_commit_exposes_and_locks_inputs_until_reveal_or_expiry() { 1646 let alice = Wallet::from_seed("blinded-lock-alice"); 1647 let bob = Wallet::from_seed("blinded-lock-bob"); 1648 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 1649 let transfer = ledger.build_transfer(&alice, bob.address(), 3, 2).unwrap(); 1650 let visible_inputs = transfer.inputs().to_vec(); 1651 1652 let blinded = ledger 1653 .build_blinded_transaction(&alice, transfer, ledger.height() + 4) 1654 .unwrap(); 1655 1656 assert_eq!(blinded.transaction.inputs.len(), visible_inputs.len()); 1657 assert_eq!( 1658 unsigned_inputs(&blinded.transaction.inputs), 1659 unsigned_inputs(&visible_inputs) 1660 ); 1661 ledger 1662 .submit_blinded_transaction(blinded.transaction) 1663 .unwrap(); 1664 let error = ledger 1665 .build_transfer(&alice, bob.address(), 1, 0) 1666 .unwrap_err(); 1667 assert!(format!("{error:#}").contains("insufficient funds")); 1668 } 1669 1670 #[test] 1671 fn blinded_payload_omits_visible_inputs_and_reconstructs_transaction_on_reveal() { 1672 let alice = Wallet::from_seed("blinded-compact-payload-alice"); 1673 let bob = Wallet::from_seed("blinded-compact-payload-bob"); 1674 let ledger = ledger_with_wallet_utxos(&alice, &[10]); 1675 let transfer = ledger.build_transfer(&alice, bob.address(), 3, 2).unwrap(); 1676 let full_transaction_bytes = serde_json::to_vec(&transfer).unwrap().len(); 1677 let blinded = ledger 1678 .build_blinded_transaction(&alice, transfer.clone(), ledger.height() + 4) 1679 .unwrap(); 1680 let key = decode_hex_array::<BLINDED_KEY_BYTES>(&blinded.reveal.key).unwrap(); 1681 let nonce = decode_hex_array::<BLINDED_NONCE_BYTES>(&blinded.transaction.nonce).unwrap(); 1682 let ciphertext = decode_hex(&blinded.transaction.ciphertext).unwrap(); 1683 1684 let plaintext = decrypt_blinded_payload( 1685 &key, 1686 &nonce, 1687 &signed_blinded_inputs(&unsigned_inputs(&blinded.transaction.inputs), ""), 1688 blinded.transaction.fee, 1689 blinded.transaction.expires_at_height, 1690 &ciphertext, 1691 ) 1692 .unwrap(); 1693 let payload: serde_json::Value = serde_json::from_slice(&plaintext).unwrap(); 1694 let revealed = decrypt_blinded_transaction(&blinded.transaction, &blinded.reveal).unwrap(); 1695 1696 assert_eq!( 1697 payload.get("kind").and_then(|kind| kind.as_str()), 1698 Some("transfer") 1699 ); 1700 assert!(payload.get("inputs").is_none()); 1701 assert!(plaintext.len() < full_transaction_bytes); 1702 assert_eq!(revealed, transfer); 1703 } 1704 1705 #[test] 1706 fn unrevealed_blinded_utxo_commit_burns_fee_and_returns_change() { 1707 let alice = Wallet::from_seed("blinded-expiry-alice"); 1708 let bob = Wallet::from_seed("blinded-expiry-bob"); 1709 let carol = Wallet::from_seed("blinded-expiry-carol"); 1710 let finalizers = [alice.clone(), bob.clone()]; 1711 let carol_balance = 10 * MICRO_IUNA; 1712 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, carol_balance)]); 1713 let fee = 100; 1714 let blinded = ledger 1715 .build_blinded_burn(&carol, 3, fee, ledger.height() + 2) 1716 .unwrap(); 1717 let commitment = blinded.transaction.commitment.clone(); 1718 ledger 1719 .submit_blinded_transaction(blinded.transaction) 1720 .unwrap(); 1721 queue_next_leader_burn(&mut ledger, &finalizers); 1722 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1723 1724 assert_eq!(ledger.balance_of(carol.address()), 0); 1725 queue_next_leader_burn(&mut ledger, &finalizers); 1726 mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); 1727 1728 assert_eq!( 1729 ledger 1730 .utxos 1731 .get(&blinded_committer_fee_outpoint(&commitment)), 1732 None 1733 ); 1734 assert_eq!( 1735 ledger 1736 .utxos 1737 .get(&blinded_expiry_change_outpoint(&commitment)), 1738 Some(&TxOutput { 1739 address: carol.address().to_string(), 1740 amount: carol_balance - fee, 1741 }) 1742 ); 1743 assert_eq!(ledger.balance_of(carol.address()), carol_balance - fee); 1744 } 1745 1746 #[test] 1747 fn fee_bearing_blinded_commit_without_inputs_is_rejected() { 1748 let alice = Wallet::from_seed("blinded-no-input-fee-alice"); 1749 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1750 let mut blinded = ledger 1751 .build_blinded_burn(&alice, 1, 1, ledger.height() + 4) 1752 .unwrap() 1753 .transaction; 1754 blinded.inputs.clear(); 1755 blinded.commitment = blinded_transaction_commitment(&blinded).unwrap(); 1756 1757 let error = ledger.submit_blinded_transaction(blinded).unwrap_err(); 1758 1759 assert!(format!("{error:#}").contains("must lock visible inputs")); 1760 } 1761 1762 #[test] 1763 fn mine_actions_cannot_be_blinded() { 1764 let alice = Wallet::from_seed("blinded-mine-collateral-alice"); 1765 let ledger = ledger_with_finalizers(&[alice.clone()], &[]); 1766 let mine = ledger.build_mine(alice.address()).unwrap(); 1767 let error = ledger 1768 .build_blinded_transaction(&alice, mine, ledger.height() + 4) 1769 .unwrap_err(); 1770 1771 assert!(format!("{error:#}").contains("mine actions are public")); 1772 } 1773 1774 #[test] 1775 fn reveal_bundle_hashes_are_bound_to_next_block_vdf_seed() { 1776 let alice = Wallet::from_seed("bundle-seed-alice"); 1777 let bob = Wallet::from_seed("bundle-seed-bob"); 1778 let carol = Wallet::from_seed("bundle-seed-carol"); 1779 let finalizers = [alice.clone(), bob.clone()]; 1780 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1781 let blinded = ledger 1782 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 1783 .unwrap(); 1784 ledger 1785 .submit_blinded_transaction(blinded.transaction.clone()) 1786 .unwrap(); 1787 queue_next_leader_burn(&mut ledger, &finalizers); 1788 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1789 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1790 queue_next_leader_burn(&mut ledger, &finalizers); 1791 let leader = ledger.expected_leader_for_next_block().unwrap(); 1792 let leader_wallet = wallet_for_address(&finalizers, &leader); 1793 let bundles = ledger 1794 .reveal_committee_for_next_block() 1795 .into_iter() 1796 .filter_map(|member| { 1797 let wallet = wallet_for_address(&finalizers, &member.owner); 1798 ledger.build_reveal_bundle(wallet).unwrap() 1799 }) 1800 .collect::<Vec<_>>(); 1801 if bundles.len() > 1 { 1802 let mut reversed = bundles.clone(); 1803 reversed.reverse(); 1804 let error = ledger 1805 .validate_next_block_reveal_bundles(reversed) 1806 .unwrap_err(); 1807 assert!(format!("{error:#}").contains("reveal bundles are not in slot order")); 1808 } 1809 1810 let without_bundles = ledger 1811 .prepare_next_block(leader_wallet.address(), ledger.tip().timestamp_ms + 1) 1812 .unwrap(); 1813 let with_bundles = ledger 1814 .prepare_next_block_with_reveal_bundles( 1815 leader_wallet.address(), 1816 ledger.tip().timestamp_ms + 1, 1817 bundles, 1818 ) 1819 .unwrap(); 1820 1821 assert_ne!(without_bundles.vdf_seed(), with_bundles.vdf_seed()); 1822 } 1823 1824 #[test] 1825 fn reveal_committee_includes_next_block_finalizer_as_slot_zero() { 1826 let alice = Wallet::from_seed("bundle-finalizer-slot-alice"); 1827 let bob = Wallet::from_seed("bundle-finalizer-slot-bob"); 1828 let carol = Wallet::from_seed("bundle-finalizer-slot-carol"); 1829 let dave = Wallet::from_seed("bundle-finalizer-slot-dave"); 1830 let erin = Wallet::from_seed("bundle-finalizer-slot-erin"); 1831 let finalizers = [alice.clone(), bob.clone(), carol.clone(), dave.clone()]; 1832 let mut ledger = ledger_with_finalizers(&finalizers, &[(&erin, 10 * MICRO_IUNA)]); 1833 let blinded = ledger 1834 .build_blinded_burn(&erin, 3, 7, ledger.height() + 4) 1835 .unwrap(); 1836 let commitment = blinded.transaction.commitment.clone(); 1837 ledger 1838 .submit_blinded_transaction(blinded.transaction) 1839 .unwrap(); 1840 queue_next_leader_burn(&mut ledger, &finalizers); 1841 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1842 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1843 queue_next_leader_burn(&mut ledger, &finalizers); 1844 1845 let leader = ledger.expected_leader_for_next_block().unwrap(); 1846 let committee = ledger.reveal_committee_for_next_block(); 1847 let leader_wallet = wallet_for_address(&finalizers, &leader); 1848 let bundle = ledger.build_reveal_bundle(leader_wallet).unwrap().unwrap(); 1849 1850 assert_eq!(committee.first().map(|member| member.slot), Some(0)); 1851 assert_eq!(committee.first().map(|member| member.rank), Some(0)); 1852 assert_eq!( 1853 committee.first().map(|member| member.owner.as_str()), 1854 Some(leader.as_str()) 1855 ); 1856 assert_eq!(bundle.slot, 0); 1857 assert_eq!(bundle.member, leader); 1858 assert!( 1859 bundle 1860 .reveals 1861 .iter() 1862 .any(|reveal| reveal.commitment == commitment) 1863 ); 1864 } 1865 1866 #[test] 1867 fn reveal_bundle_section_deduplicates_reveals_with_slot_mask() { 1868 let alice = Wallet::from_seed("bundle-compact-alice"); 1869 let bob = Wallet::from_seed("bundle-compact-bob"); 1870 let carol = Wallet::from_seed("bundle-compact-carol"); 1871 let dave = Wallet::from_seed("bundle-compact-dave"); 1872 let finalizers = [alice.clone(), bob.clone(), carol.clone()]; 1873 let mut ledger = ledger_with_finalizers(&finalizers, &[(&dave, 10 * MICRO_IUNA)]); 1874 let blinded = ledger 1875 .build_blinded_burn(&dave, 3, 7, ledger.height() + 4) 1876 .unwrap(); 1877 ledger 1878 .submit_blinded_transaction(blinded.transaction.clone()) 1879 .unwrap(); 1880 queue_next_leader_burn(&mut ledger, &finalizers); 1881 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1882 ledger 1883 .submit_blinded_reveal(blinded.reveal.clone()) 1884 .unwrap(); 1885 queue_next_leader_burn(&mut ledger, &finalizers); 1886 1887 let mut bundles = ledger 1888 .reveal_committee_for_next_block() 1889 .into_iter() 1890 .filter_map(|member| { 1891 let wallet = wallet_for_address(&finalizers, &member.owner); 1892 ledger.build_reveal_bundle(wallet).unwrap() 1893 }) 1894 .collect::<Vec<_>>(); 1895 assert!(bundles.len() >= 2); 1896 bundles.truncate(2); 1897 let expected_hashes = reveal_bundle_hashes(&bundles); 1898 let expected_mask = bundles 1899 .iter() 1900 .fold(0_u8, |mask, bundle| mask | (1_u8 << bundle.slot)); 1901 1902 let leader = ledger.expected_leader_for_next_block().unwrap(); 1903 let leader_wallet = wallet_for_address(&finalizers, &leader); 1904 let prepared = ledger 1905 .prepare_next_block_with_reveal_bundles( 1906 leader_wallet.address(), 1907 ledger.tip().timestamp_ms + 1, 1908 bundles.clone(), 1909 ) 1910 .unwrap(); 1911 let block = prepared.finish(leader_wallet, "preverified-vdf".to_string()); 1912 1913 assert_eq!(block.reveal_bundle_section.signatures.len(), 2); 1914 assert_eq!(block.reveal_bundle_section.reveals.len(), 1); 1915 assert_eq!( 1916 block.reveal_bundle_section.reveals[0].bundle_mask, 1917 expected_mask 1918 ); 1919 assert_eq!(block.all_blinded_reveals(), vec![&blinded.reveal]); 1920 assert_eq!(block.reveal_bundle_hashes(), expected_hashes); 1921 assert_eq!( 1922 block 1923 .reveal_bundle_section 1924 .expand(block.height, &block.prev_hash), 1925 bundles 1926 ); 1927 } 1928 1929 #[test] 1930 fn reveal_bundle_validation_rejects_wrong_signature_and_slot() { 1931 let alice = Wallet::from_seed("bundle-invalid-alice"); 1932 let bob = Wallet::from_seed("bundle-invalid-bob"); 1933 let carol = Wallet::from_seed("bundle-invalid-carol"); 1934 let finalizers = [alice.clone(), bob.clone()]; 1935 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1936 let blinded = ledger 1937 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 1938 .unwrap(); 1939 ledger 1940 .submit_blinded_transaction(blinded.transaction.clone()) 1941 .unwrap(); 1942 queue_next_leader_burn(&mut ledger, &finalizers); 1943 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1944 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1945 let member = ledger.reveal_committee_for_next_block()[0].clone(); 1946 let wallet = wallet_for_address(&finalizers, &member.owner); 1947 let bundle = ledger.build_reveal_bundle(wallet).unwrap().unwrap(); 1948 1949 let mut wrong_signature = bundle.clone(); 1950 wrong_signature.signature = "00".repeat(SIGNATURE_BYTES); 1951 let error = ledger 1952 .validate_next_block_reveal_bundles(vec![wrong_signature]) 1953 .unwrap_err(); 1954 assert!(format!("{error:#}").contains("reveal bundle signature is invalid")); 1955 1956 let mut wrong_slot = bundle; 1957 wrong_slot.slot = REVEAL_COMMITTEE_SIZE as u8 - 1; 1958 let error = ledger 1959 .validate_next_block_reveal_bundles(vec![wrong_slot]) 1960 .unwrap_err(); 1961 assert!( 1962 format!("{error:#}").contains("reveal bundle slot is not assigned") 1963 || format!("{error:#}").contains("reveal bundle member is not assigned to slot") 1964 ); 1965 } 1966 1967 #[test] 1968 fn blinded_reveal_with_wrong_key_is_rejected_in_block() { 1969 let alice = Wallet::from_seed("blinded-wrong-key-finalizer-alice"); 1970 let bob = Wallet::from_seed("blinded-wrong-key-finalizer-bob"); 1971 let carol = Wallet::from_seed("blinded-wrong-key-carol"); 1972 let finalizers = [alice.clone(), bob.clone()]; 1973 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1974 let blinded = ledger 1975 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 1976 .unwrap(); 1977 ledger 1978 .submit_blinded_transaction(blinded.transaction.clone()) 1979 .unwrap(); 1980 queue_next_leader_burn(&mut ledger, &finalizers); 1981 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1982 1983 let leader = ledger.expected_leader_for_next_block().unwrap(); 1984 let wallet = wallet_for_address(&finalizers, &leader); 1985 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 1986 ledger.submit_transaction(filler_burn).unwrap(); 1987 let mut prepared = ledger 1988 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 1989 .unwrap(); 1990 let committee_member = ledger.reveal_committee_for_next_block()[0].clone(); 1991 let committee_wallet = wallet_for_address(&finalizers, &committee_member.owner); 1992 let wrong_reveal = BlindedReveal { 1993 commitment: blinded.transaction.commitment, 1994 key: "00".repeat(BLINDED_KEY_BYTES), 1995 }; 1996 let wrong_bundle = committee_wallet.reveal_bundle(RevealBundlePayload { 1997 height: prepared.height, 1998 prev_hash: prepared.prev_hash.clone(), 1999 slot: committee_member.slot, 2000 member: committee_wallet.address().to_string(), 2001 reveals: vec![wrong_reveal], 2002 }); 2003 prepared.reveal_bundle_section = ledger.reveal_bundle_section_from_bundles(vec![wrong_bundle]); 2004 prepared.reward = blinded_reveal_finalizer_fee( 2005 blinded.transaction.fee, 2006 prepared.reveal_bundle_section.signatures.len(), 2007 ledger.reveal_committee_for_next_block().len(), 2008 ); 2009 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 2010 2011 let error = ledger 2012 .apply_preverified_block_at(block, u64::MAX) 2013 .unwrap_err(); 2014 2015 assert!( 2016 format!("{error:#}").contains("decrypt blinded transaction"), 2017 "{error:#}" 2018 ); 2019 } 2020 2021 #[test] 2022 fn expired_blinded_reveal_is_not_selected() { 2023 let alice = Wallet::from_seed("blinded-expire-finalizer-alice"); 2024 let bob = Wallet::from_seed("blinded-expire-finalizer-bob"); 2025 let carol = Wallet::from_seed("blinded-expire-carol"); 2026 let finalizers = [alice.clone(), bob.clone()]; 2027 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2028 let blinded = ledger 2029 .build_blinded_burn(&carol, 3, 7, ledger.height() + 2) 2030 .unwrap(); 2031 ledger 2032 .submit_blinded_transaction(blinded.transaction.clone()) 2033 .unwrap(); 2034 queue_next_leader_burn(&mut ledger, &finalizers); 2035 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2036 2037 let leader = ledger.expected_leader_for_next_block().unwrap(); 2038 let wallet = wallet_for_address(&finalizers, &leader); 2039 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2040 ledger.submit_transaction(filler_burn).unwrap(); 2041 mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); 2042 2043 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2044 assert!(ledger.valid_pending_blinded_reveals().is_empty()); 2045 } 2046 2047 #[test] 2048 fn recovery_block_includes_pending_blinded_transactions_when_space_allows() { 2049 let alice = Wallet::from_seed("recovery-blinded-commit-alice"); 2050 let bob = Wallet::from_seed("recovery-blinded-commit-bob"); 2051 let carol = Wallet::from_seed("recovery-blinded-commit-carol"); 2052 let mut ledger = ledger_with_finalizers( 2053 &[alice], 2054 &[(&bob, 10 * MICRO_IUNA), (&carol, 10 * MICRO_IUNA)], 2055 ); 2056 let blinded = ledger 2057 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2058 .unwrap(); 2059 ledger 2060 .submit_blinded_transaction(blinded.transaction.clone()) 2061 .unwrap(); 2062 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2063 ledger.submit_transaction(recovery_burn).unwrap(); 2064 2065 let block = ledger 2066 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 2067 .unwrap(); 2068 2069 assert!( 2070 block 2071 .blinded_transactions 2072 .iter() 2073 .any(|transaction| transaction.commitment == blinded.transaction.commitment) 2074 ); 2075 } 2076 2077 #[test] 2078 fn recovery_block_size_selection_uses_recovery_skeleton() { 2079 let alice = Wallet::from_seed("recovery-size-commit-alice"); 2080 let bob = Wallet::from_seed("recovery-size-commit-bob"); 2081 let carol = Wallet::from_seed("recovery-size-commit-carol"); 2082 let mut ledger = ledger_with_finalizers( 2083 &[alice], 2084 &[(&bob, 10 * MICRO_IUNA), (&carol, 10 * MICRO_IUNA)], 2085 ); 2086 let blinded = ledger 2087 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2088 .unwrap(); 2089 ledger 2090 .submit_blinded_transaction(blinded.transaction.clone()) 2091 .unwrap(); 2092 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2093 ledger.submit_transaction(recovery_burn.clone()).unwrap(); 2094 let recovery_selection = BlockSelection { 2095 transactions: vec![recovery_burn], 2096 blinded_transactions: vec![blinded.transaction.clone()], 2097 }; 2098 let recovery_estimate = 2099 estimated_block_selection_size_bytes(&recovery_selection, true).unwrap(); 2100 let ticket_estimate = estimated_block_selection_size_bytes(&recovery_selection, false).unwrap(); 2101 assert!(recovery_estimate < ticket_estimate); 2102 2103 ledger.launch_profile.max_block_bytes = recovery_estimate; 2104 let tight_block = ledger 2105 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 2106 .unwrap(); 2107 2108 assert!( 2109 tight_block 2110 .blinded_transactions 2111 .iter() 2112 .any(|transaction| transaction.commitment == blinded.transaction.commitment) 2113 ); 2114 } 2115 2116 #[test] 2117 fn recovery_block_includes_pending_blinded_reveals_when_space_allows() { 2118 let alice = Wallet::from_seed("recovery-blinded-reveal-alice"); 2119 let bob = Wallet::from_seed("recovery-blinded-reveal-bob"); 2120 let carol = Wallet::from_seed("recovery-blinded-reveal-carol"); 2121 let finalizers = [alice.clone(), bob.clone()]; 2122 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2123 let blinded = ledger 2124 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2125 .unwrap(); 2126 ledger 2127 .submit_blinded_transaction(blinded.transaction.clone()) 2128 .unwrap(); 2129 queue_next_leader_burn(&mut ledger, &finalizers); 2130 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2131 ledger 2132 .submit_blinded_reveal(blinded.reveal.clone()) 2133 .unwrap(); 2134 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2135 ledger.submit_transaction(recovery_burn).unwrap(); 2136 2137 let bundles = ledger 2138 .reveal_committee_for_next_block() 2139 .into_iter() 2140 .filter_map(|member| { 2141 let wallet = wallet_for_address(&finalizers, &member.owner); 2142 ledger.build_reveal_bundle(wallet).unwrap() 2143 }) 2144 .collect::<Vec<_>>(); 2145 let prepared = ledger 2146 .prepare_recovery_block_with_reveal_bundles( 2147 bob.address(), 2148 ledger.recovery_block_min_timestamp(), 2149 bundles, 2150 ) 2151 .unwrap(); 2152 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 2153 let block = prepared.finish(&bob, vdf_output); 2154 2155 assert!( 2156 block 2157 .all_blinded_reveals() 2158 .iter() 2159 .any(|reveal| reveal.commitment == blinded.transaction.commitment) 2160 ); 2161 } 2162 2163 #[test] 2164 fn blinded_transaction_expiring_at_next_height_is_not_selected() { 2165 let alice = Wallet::from_seed("blinded-next-expire-finalizer-alice"); 2166 let bob = Wallet::from_seed("blinded-next-expire-finalizer-bob"); 2167 let carol = Wallet::from_seed("blinded-next-expire-carol"); 2168 let finalizers = [alice.clone(), bob.clone()]; 2169 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2170 let blinded = ledger 2171 .build_blinded_burn(&carol, 3, 7, ledger.height() + 1) 2172 .unwrap(); 2173 ledger 2174 .submit_blinded_transaction(blinded.transaction) 2175 .unwrap(); 2176 2177 let leader = ledger.expected_leader_for_next_block().unwrap(); 2178 let wallet = wallet_for_address(&finalizers, &leader); 2179 let error = ledger.prepare_next_block(wallet.address(), 1).unwrap_err(); 2180 2181 assert!(format!("{error:#}").contains("block must include at least one burn transaction")); 2182 } 2183 2184 #[test] 2185 fn blinded_transaction_expiry_cannot_exceed_protocol_window() { 2186 let alice = Wallet::from_seed("blinded-window-finalizer-alice"); 2187 let bob = Wallet::from_seed("blinded-window-finalizer-bob"); 2188 let carol = Wallet::from_seed("blinded-window-carol"); 2189 let finalizers = [alice, bob]; 2190 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2191 let max_expiry = ledger 2192 .height() 2193 .saturating_add(MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS); 2194 2195 ledger.build_blinded_burn(&carol, 3, 7, max_expiry).unwrap(); 2196 2197 let error = ledger 2198 .build_blinded_burn(&carol, 3, 7, max_expiry + 1) 2199 .unwrap_err(); 2200 assert!(format!("{error:#}").contains("expiry is too far in the future")); 2201 2202 let mut forged = ledger 2203 .build_blinded_burn(&carol, 3, 7, max_expiry) 2204 .unwrap() 2205 .transaction; 2206 forged.expires_at_height = max_expiry + 1; 2207 forged.commitment = blinded_transaction_commitment(&forged).unwrap(); 2208 let error = ledger.submit_blinded_transaction(forged).unwrap_err(); 2209 assert!(format!("{error:#}").contains("expiry is too far in the future")); 2210 } 2211 2212 #[test] 2213 fn blinded_transaction_does_not_satisfy_plaintext_burn_requirement() { 2214 let alice = Wallet::from_seed("blinded-no-burn-finalizer-alice"); 2215 let bob = Wallet::from_seed("blinded-no-burn-finalizer-bob"); 2216 let carol = Wallet::from_seed("blinded-no-burn-carol"); 2217 let finalizers = [alice.clone(), bob.clone()]; 2218 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2219 let blinded = ledger 2220 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2221 .unwrap(); 2222 ledger 2223 .submit_blinded_transaction(blinded.transaction) 2224 .unwrap(); 2225 2226 let leader = ledger.expected_leader_for_next_block().unwrap(); 2227 let wallet = wallet_for_address(&finalizers, &leader); 2228 let error = ledger.prepare_next_block(wallet.address(), 1).unwrap_err(); 2229 2230 assert!(format!("{error:#}").contains("block must include at least one burn transaction")); 2231 } 2232 2233 #[test] 2234 fn revealed_blinded_transaction_cannot_be_included_again() { 2235 let alice = Wallet::from_seed("blinded-duplicate-finalizer-alice"); 2236 let bob = Wallet::from_seed("blinded-duplicate-finalizer-bob"); 2237 let carol = Wallet::from_seed("blinded-duplicate-carol"); 2238 let finalizers = [alice.clone(), bob.clone()]; 2239 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2240 let blinded = ledger 2241 .build_blinded_burn(&carol, 3, 7, ledger.height() + 6) 2242 .unwrap(); 2243 ledger 2244 .submit_blinded_transaction(blinded.transaction.clone()) 2245 .unwrap(); 2246 queue_next_leader_burn(&mut ledger, &finalizers); 2247 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2248 ledger 2249 .submit_blinded_reveal(blinded.reveal.clone()) 2250 .unwrap(); 2251 queue_next_leader_burn(&mut ledger, &finalizers); 2252 mine_preverified_as_next_leader_with_reveal_bundles(&mut ledger, &finalizers, 2); 2253 2254 let leader = ledger.expected_leader_for_next_block().unwrap(); 2255 let wallet = wallet_for_address(&finalizers, &leader); 2256 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2257 ledger.submit_transaction(filler_burn).unwrap(); 2258 let mut prepared = ledger 2259 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 2260 .unwrap(); 2261 prepared 2262 .blinded_transactions 2263 .push(blinded.transaction.clone()); 2264 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 2265 2266 let error = ledger 2267 .apply_preverified_block_at(block, u64::MAX) 2268 .unwrap_err(); 2269 2270 assert!(format!("{error:#}").contains("blinded transaction is already on chain")); 2271 } 2272 2273 #[test] 2274 fn pending_blinded_reveals_expose_revealed_transaction_data() { 2275 let alice = Wallet::from_seed("pending-reveal-data-finalizer-alice"); 2276 let bob = Wallet::from_seed("pending-reveal-data-finalizer-bob"); 2277 let carol = Wallet::from_seed("pending-reveal-data-carol"); 2278 let finalizers = [alice.clone(), bob.clone()]; 2279 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2280 let blinded = ledger 2281 .build_blinded_burn(&carol, 3, 7, ledger.height() + 6) 2282 .unwrap(); 2283 let commitment = blinded.transaction.commitment.clone(); 2284 ledger 2285 .submit_blinded_transaction(blinded.transaction) 2286 .unwrap(); 2287 queue_next_leader_burn(&mut ledger, &finalizers); 2288 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2289 2290 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2291 2292 let revealed = ledger.pending_revealed_blinded_transactions(); 2293 assert_eq!(revealed.len(), 1); 2294 assert_eq!(revealed[0].commitment, commitment); 2295 assert_eq!(revealed[0].height, ledger.height() + 1); 2296 assert_eq!(revealed[0].transaction.amount(), 3); 2297 assert_eq!(revealed[0].transaction.fee(), 7); 2298 assert!(revealed[0].transaction.is_burn()); 2299 } 2300 2301 #[test] 2302 fn abandoned_fork_blinded_transactions_return_to_mempool() { 2303 let alice = Wallet::from_seed("blinded-reorg-finalizer-alice"); 2304 let bob = Wallet::from_seed("blinded-reorg-finalizer-bob"); 2305 let carol = Wallet::from_seed("blinded-reorg-carol"); 2306 let finalizers = [alice.clone(), bob.clone()]; 2307 let mut local = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2308 let mut remote = local.clone(); 2309 let blinded = local 2310 .build_blinded_burn(&carol, 3, 7, local.height() + 8) 2311 .unwrap(); 2312 local 2313 .submit_blinded_transaction(blinded.transaction.clone()) 2314 .unwrap(); 2315 queue_next_leader_burn(&mut local, &finalizers); 2316 mine_preverified_as_next_leader(&mut local, &finalizers, 1); 2317 2318 for timestamp_ms in [1, 2] { 2319 let leader = remote.expected_leader_for_next_block().unwrap(); 2320 let wallet = wallet_for_address(&finalizers, &leader); 2321 let burn = remote.build_burn(wallet, 1, 0).unwrap(); 2322 remote.submit_transaction(burn).unwrap(); 2323 mine_preverified_as_next_leader(&mut remote, &finalizers, timestamp_ms); 2324 } 2325 2326 assert!( 2327 local 2328 .extend_from_preverified_snapshot_at(remote.snapshot(), u64::MAX) 2329 .unwrap() 2330 ); 2331 assert!(local.has_blinded_transaction(&blinded.transaction.commitment)); 2332 assert_eq!( 2333 local.pending_blinded_transactions(), 2334 std::slice::from_ref(&blinded.transaction) 2335 ); 2336 } 2337 2338 #[test] 2339 fn block_selection_includes_mine_action_after_required_block_burn() { 2340 let alice = Wallet::from_seed("mine-fixed-reward-select-alice"); 2341 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2342 2343 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2344 ledger.submit_transaction(burn).unwrap(); 2345 let mine = ledger.build_mine(alice.address()).unwrap(); 2346 ledger.submit_transaction(mine.clone()).unwrap(); 2347 2348 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2349 2350 assert_eq!( 2351 block.transactions.iter().filter(|tx| tx.is_burn()).count(), 2352 1 2353 ); 2354 assert!( 2355 block 2356 .transactions 2357 .iter() 2358 .any(|tx| tx.signature() == mine.signature()) 2359 ); 2360 assert_eq!(block.reward, MINE_FINALIZER_FEE); 2361 } 2362 2363 #[test] 2364 fn block_selection_can_skip_mine_action_when_space_is_limited() { 2365 let alice = Wallet::from_seed("mine-space-limit-alice"); 2366 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2367 ledger.launch_profile.max_block_transactions = 2; 2368 2369 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2370 ledger.submit_transaction(burn).unwrap(); 2371 let first_mine = ledger.build_mine(alice.address()).unwrap(); 2372 ledger.submit_transaction(first_mine).unwrap(); 2373 let second_mine = ledger.build_mine(alice.address()).unwrap(); 2374 ledger.submit_transaction(second_mine).unwrap(); 2375 2376 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2377 2378 assert_eq!(block.transactions.len(), 2); 2379 assert!(block.transactions.iter().any(Transaction::is_burn)); 2380 assert_eq!(block.reward, MINE_FINALIZER_FEE); 2381 } 2382 2383 #[test] 2384 fn pending_mine_outputs_are_not_spendable_until_confirmed() { 2385 let alice = Wallet::from_seed("pending-mine-spend-alice"); 2386 let bob = Wallet::from_seed("pending-mine-spend-bob"); 2387 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2388 2389 let mine = ledger.build_mine(alice.address()).unwrap(); 2390 let mine_outpoint = OutPoint { 2391 txid: mine.signature().to_string(), 2392 index: 0, 2393 }; 2394 ledger.submit_transaction(mine.clone()).unwrap(); 2395 2396 assert!( 2397 !ledger 2398 .available_utxos_for_address(alice.address()) 2399 .unwrap() 2400 .iter() 2401 .any(|(outpoint, _)| outpoint == &mine_outpoint) 2402 ); 2403 let pending_error = ledger 2404 .build_transfer_with_inputs( 2405 &alice, 2406 bob.address(), 2407 TEST_BURN_AMOUNT, 2408 0, 2409 std::slice::from_ref(&mine_outpoint), 2410 ) 2411 .unwrap_err(); 2412 assert!(format!("{pending_error:#}").contains("not spendable")); 2413 2414 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2415 ledger.submit_transaction(burn).unwrap(); 2416 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2417 assert!( 2418 block 2419 .transactions 2420 .iter() 2421 .any(|tx| tx.signature() == mine.signature()) 2422 ); 2423 ledger.apply_locally_mined_block(block).unwrap(); 2424 2425 assert!( 2426 ledger 2427 .available_utxos_for_address(alice.address()) 2428 .unwrap() 2429 .iter() 2430 .any(|(outpoint, _)| outpoint == &mine_outpoint) 2431 ); 2432 ledger 2433 .build_transfer_with_inputs( 2434 &alice, 2435 bob.address(), 2436 TEST_BURN_AMOUNT, 2437 0, 2438 std::slice::from_ref(&mine_outpoint), 2439 ) 2440 .unwrap(); 2441 } 2442 2443 #[test] 2444 fn burns_built_after_pending_mine_do_not_spend_pending_mine_output() { 2445 let alice = Wallet::from_seed("pending-mine-burn-alice"); 2446 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2447 2448 let mine = ledger.build_mine(alice.address()).unwrap(); 2449 let mine_outpoint = OutPoint { 2450 txid: mine.signature().to_string(), 2451 index: 0, 2452 }; 2453 ledger.submit_transaction(mine).unwrap(); 2454 2455 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2456 2457 let Transaction::Burn { inputs, .. } = &burn else { 2458 panic!("expected burn transaction"); 2459 }; 2460 assert!(!inputs.iter().any(|input| input.outpoint == mine_outpoint)); 2461 } 2462 2463 #[test] 2464 fn pending_blinded_transactions_with_spent_inputs_are_pruned_after_block_apply() { 2465 let alice = Wallet::from_seed("pending-blind-spent-prune-alice"); 2466 let mut mempool_ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2467 let mut block_ledger = mempool_ledger.clone(); 2468 let amount = mempool_ledger.balance_of(alice.address()); 2469 let blinded = mempool_ledger 2470 .build_blinded_burn(&alice, amount, 0, mempool_ledger.height() + 4) 2471 .unwrap(); 2472 mempool_ledger 2473 .submit_blinded_transaction(blinded.transaction.clone()) 2474 .unwrap(); 2475 2476 let burn = block_ledger.build_burn(&alice, amount, 0).unwrap(); 2477 let Transaction::Burn { inputs, .. } = &burn else { 2478 panic!("expected burn transaction"); 2479 }; 2480 assert!(blinded.transaction.inputs.iter().any(|input| { 2481 inputs 2482 .iter() 2483 .any(|burn_input| burn_input.outpoint == input.outpoint) 2484 })); 2485 block_ledger.submit_transaction(burn).unwrap(); 2486 let block = block_ledger.mine_next_block(&alice, 1).unwrap(); 2487 2488 mempool_ledger.apply_block(block).unwrap(); 2489 2490 assert!(mempool_ledger.pending_blinded_transactions().is_empty()); 2491 } 2492 2493 #[test] 2494 fn block_selection_limits_mine_actions_per_anchor() { 2495 let alice = Wallet::from_seed("mine-anchor-limit-selection-alice"); 2496 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2497 2498 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2499 ledger.submit_transaction(burn).unwrap(); 2500 let first_mine = ledger.build_mine(alice.address()).unwrap(); 2501 ledger.submit_transaction(first_mine.clone()).unwrap(); 2502 let second_mine = ledger.build_mine(alice.address()).unwrap(); 2503 ledger.submit_transaction(second_mine.clone()).unwrap(); 2504 assert_eq!(ledger.pending().len(), 3); 2505 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2506 2507 assert_eq!(block.transactions.len(), 3); 2508 assert!(block.transactions.iter().any(Transaction::is_burn)); 2509 let included_mines = block 2510 .transactions 2511 .iter() 2512 .filter(|transaction| matches!(transaction, Transaction::Mine { .. })) 2513 .count(); 2514 assert_eq!(included_mines, MINE_ACTIONS_PER_ANCHOR_LIMIT); 2515 assert!( 2516 block 2517 .transactions 2518 .iter() 2519 .any(|tx| tx.signature() == first_mine.signature()) 2520 ); 2521 assert!( 2522 block 2523 .transactions 2524 .iter() 2525 .any(|tx| tx.signature() == second_mine.signature()) 2526 ); 2527 assert_ne!(first_mine.signature(), second_mine.signature()); 2528 assert_eq!(block.reward, first_mine.fee() + second_mine.fee()); 2529 } 2530 2531 #[test] 2532 fn blocks_reject_too_many_mine_actions_for_one_anchor() { 2533 let alice = Wallet::from_seed("mine-anchor-limit-block-alice"); 2534 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2535 2536 let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); 2537 let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); 2538 let third_mine = test_mine_with_salt(&ledger, alice.address(), 3); 2539 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2540 ledger.submit_transaction(burn).unwrap(); 2541 let mut block = ledger 2542 .prepare_next_block( 2543 alice.address(), 2544 ledger 2545 .tip() 2546 .timestamp_ms 2547 .saturating_add(VDF_TARGET_BLOCK_MS), 2548 ) 2549 .unwrap() 2550 .finish(&alice, "preverified-vdf".to_string()); 2551 block.transactions.push(first_mine); 2552 block.transactions.push(second_mine); 2553 block.transactions.push(third_mine); 2554 block.reward = fee_reward(&block.transactions).unwrap(); 2555 block.hash = block.compute_hash(); 2556 2557 let error = ledger 2558 .apply_preverified_block_at(block, u64::MAX) 2559 .unwrap_err(); 2560 2561 assert!(format!("{error:#}").contains("mine actions per anchor limit")); 2562 } 2563 2564 #[test] 2565 fn mempool_rejects_mine_actions_above_anchor_limit() { 2566 let alice = Wallet::from_seed("mine-anchor-limit-mempool-alice"); 2567 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2568 2569 let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); 2570 let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); 2571 let third_mine = test_mine_with_salt(&ledger, alice.address(), 3); 2572 ledger.submit_transaction(first_mine).unwrap(); 2573 ledger.submit_transaction(second_mine).unwrap(); 2574 let error = ledger.submit_transaction(third_mine).unwrap_err(); 2575 2576 assert!(format!("{error:#}").contains("mine transaction anchor limit reached")); 2577 } 2578 2579 #[test] 2580 fn mine_difficulty_increases_when_issuance_exceeds_target_window() { 2581 let alice = Wallet::from_seed("mine-difficulty-up-alice"); 2582 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2583 2584 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2585 apply_preverified_burn_block_with_mines(&mut ledger, &alice, 2); 2586 } 2587 2588 assert_eq!( 2589 ledger.current_mine_difficulty_bits(), 2590 MINE_DIFFICULTY_BITS + 1 2591 ); 2592 let mine = ledger.build_mine(alice.address()).unwrap(); 2593 let Transaction::Mine { 2594 difficulty_bits, .. 2595 } = mine 2596 else { 2597 panic!("expected mine action"); 2598 }; 2599 assert_eq!(difficulty_bits, MINE_DIFFICULTY_BITS + 1); 2600 } 2601 2602 #[test] 2603 fn mine_difficulty_decreases_when_issuance_is_below_target_window() { 2604 let alice = Wallet::from_seed("mine-difficulty-down-alice"); 2605 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2606 2607 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2608 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2609 } 2610 2611 assert_eq!( 2612 ledger.current_mine_difficulty_bits(), 2613 MINE_DIFFICULTY_BITS - MINE_MAX_RETARGET_STEP_BITS 2614 ); 2615 2616 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2617 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2618 } 2619 2620 assert_eq!( 2621 ledger.current_mine_difficulty_bits(), 2622 MINE_MIN_DIFFICULTY_BITS 2623 ); 2624 2625 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2626 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2627 } 2628 2629 assert_eq!( 2630 ledger.current_mine_difficulty_bits(), 2631 MINE_MIN_DIFFICULTY_BITS 2632 ); 2633 } 2634 2635 #[test] 2636 fn mine_actions_expire_when_anchor_is_too_old() { 2637 let alice = Wallet::from_seed("mine-anchor-expiry-alice"); 2638 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2639 let stale_mine = ledger.build_mine(alice.address()).unwrap(); 2640 2641 for _ in 0..=MINE_MAX_ANCHOR_AGE_BLOCKS { 2642 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2643 } 2644 2645 let error = ledger.submit_transaction(stale_mine).unwrap_err(); 2646 assert!(format!("{error:#}").contains("mine transaction anchor is too old")); 2647 } 2648 2649 #[test] 2650 fn pending_mine_actions_are_removed_when_anchor_expires() { 2651 let alice = Wallet::from_seed("pending-mine-anchor-expiry-alice"); 2652 let bob = Wallet::from_seed("pending-mine-anchor-expiry-bob"); 2653 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2654 ledger.launch_profile.max_block_transactions = 1; 2655 let stale_mine = ledger.build_mine(bob.address()).unwrap(); 2656 ledger.submit_transaction(stale_mine.clone()).unwrap(); 2657 2658 for _ in 0..=MINE_MAX_ANCHOR_AGE_BLOCKS { 2659 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2660 } 2661 2662 assert!( 2663 ledger 2664 .pending() 2665 .iter() 2666 .all(|tx| tx.signature() != stale_mine.signature()) 2667 ); 2668 } 2669 2670 #[test] 2671 fn stratum_mine_header_proof_is_validated() { 2672 let alice = Wallet::from_seed("stratum-proof-alice"); 2673 let ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2674 let anchor = ledger.tip().hash.clone(); 2675 let difficulty_bits = ledger.current_mine_difficulty_bits(); 2676 let template = ledger 2677 .stratum_mine_template(alice.address(), anchor, 1, difficulty_bits) 2678 .unwrap(); 2679 2680 let mut accepted = None; 2681 for nonce in 0_u32..50_000 { 2682 let result = ledger.build_stratum_mine( 2683 template.clone(), 2684 StratumMineShare { 2685 extranonce2: [0, 0, 0, 0], 2686 header_nonce: nonce.to_le_bytes(), 2687 }, 2688 ); 2689 if let Ok(tx) = result { 2690 accepted = Some(tx); 2691 break; 2692 } 2693 } 2694 2695 let tx = accepted.expect("expected Stratum proof within search range"); 2696 let Transaction::Mine { 2697 proof_header, 2698 signature, 2699 .. 2700 } = tx 2701 else { 2702 panic!("expected mine action"); 2703 }; 2704 assert_eq!(proof_header.as_deref().unwrap_or_default().len(), 160); 2705 assert!(hash_meets_difficulty(&signature, difficulty_bits)); 2706 } 2707 2708 #[test] 2709 fn stratum_mine_salt_allows_multiple_actions_for_same_anchor() { 2710 let alice = Wallet::from_seed("stratum-salt-alice"); 2711 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2712 let anchor = ledger.tip().hash.clone(); 2713 let difficulty_bits = ledger.current_mine_difficulty_bits(); 2714 2715 for salt in [1, 2] { 2716 let template = ledger 2717 .stratum_mine_template(alice.address(), anchor.clone(), salt, difficulty_bits) 2718 .unwrap(); 2719 let mut accepted = None; 2720 for nonce in 0_u32..50_000 { 2721 let result = ledger.build_stratum_mine( 2722 template.clone(), 2723 StratumMineShare { 2724 extranonce2: [0, 0, 0, 0], 2725 header_nonce: nonce.to_le_bytes(), 2726 }, 2727 ); 2728 if let Ok(tx) = result { 2729 accepted = Some(tx); 2730 break; 2731 } 2732 } 2733 let tx = accepted.expect("expected Stratum proof within search range"); 2734 assert!(ledger.submit_transaction(tx).unwrap()); 2735 } 2736 2737 assert_eq!(ledger.pending().len(), 2); 2738 let salts = ledger 2739 .pending() 2740 .iter() 2741 .map(|tx| match tx { 2742 Transaction::Mine { salt, .. } => *salt, 2743 _ => panic!("expected mine action"), 2744 }) 2745 .collect::<BTreeSet<_>>(); 2746 assert_eq!(salts, BTreeSet::from([1, 2])); 2747 }