tests.rs (102132B)
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 parent = ticket_test_block(3, FinalizerMode::Ticket, 0, "parent"); 1290 let mut tickets = vec![ 1291 BurnTicket { 1292 id: "high-burn".to_string(), 1293 owner: "alice".to_string(), 1294 amount: 10_000, 1295 eligible_from_height: 4, 1296 eligible_until_height: 6, 1297 }, 1298 BurnTicket { 1299 id: "small-burn".to_string(), 1300 owner: "bob".to_string(), 1301 amount: 1, 1302 eligible_from_height: 5, 1303 eligible_until_height: 7, 1304 }, 1305 ]; 1306 let block = ticket_test_child(&parent, 4, "alice", 0, "high-burn"); 1307 1308 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1309 1310 assert!( 1311 tickets.iter().all(|ticket| ticket.id != "high-burn"), 1312 "a winning burn must not remain eligible for the rest of its window" 1313 ); 1314 assert!( 1315 tickets.iter().any(|ticket| ticket.id == "small-burn"), 1316 "unselected future tickets should remain pending" 1317 ); 1318 } 1319 1320 fn ticket_test_block( 1321 height: u64, 1322 finalizer_mode: FinalizerMode, 1323 finalizer_rank: u32, 1324 seed: &str, 1325 ) -> Block { 1326 let mut block = Block { 1327 height, 1328 prev_hash: hex_hash(format!("ticket-test-prev:{seed}:{height}")), 1329 timestamp_ms: height, 1330 miner: seed.to_string(), 1331 finalizer_mode, 1332 finalizer_rank, 1333 reward: BLOCK_REWARD, 1334 vdf_rounds: 1, 1335 vdf_output: hex_hash(format!("ticket-test-vdf:{seed}:{height}")), 1336 leader_proof: None, 1337 blinded_transactions: Vec::new(), 1338 reveal_bundle_section: RevealBundleSection::default(), 1339 transactions: Vec::new(), 1340 hash: String::new(), 1341 }; 1342 block.hash = block.compute_hash(); 1343 block 1344 } 1345 1346 fn ticket_test_child( 1347 parent: &Block, 1348 height: u64, 1349 miner: &str, 1350 finalizer_rank: u32, 1351 ticket_id: &str, 1352 ) -> Block { 1353 let mut block = ticket_test_block(height, FinalizerMode::Ticket, finalizer_rank, miner); 1354 block.prev_hash = parent.hash.clone(); 1355 block.leader_proof = Some(LeaderProof { 1356 ticket_id: ticket_id.to_string(), 1357 public_key: miner.to_string(), 1358 signature: "signature".to_string(), 1359 }); 1360 block.hash = block.compute_hash(); 1361 block 1362 } 1363 1364 fn fallback_invalidation_tickets(height: u64) -> Vec<BurnTicket> { 1365 vec![ 1366 BurnTicket { 1367 id: "alice-main".to_string(), 1368 owner: "alice".to_string(), 1369 amount: 10, 1370 eligible_from_height: height, 1371 eligible_until_height: height + 2, 1372 }, 1373 BurnTicket { 1374 id: "alice-other-eligible".to_string(), 1375 owner: "alice".to_string(), 1376 amount: 7, 1377 eligible_from_height: height, 1378 eligible_until_height: height + 2, 1379 }, 1380 BurnTicket { 1381 id: "bob-main".to_string(), 1382 owner: "bob".to_string(), 1383 amount: 9, 1384 eligible_from_height: height, 1385 eligible_until_height: height + 2, 1386 }, 1387 BurnTicket { 1388 id: "bob-other-eligible".to_string(), 1389 owner: "bob".to_string(), 1390 amount: 6, 1391 eligible_from_height: height, 1392 eligible_until_height: height + 2, 1393 }, 1394 BurnTicket { 1395 id: "carol-main".to_string(), 1396 owner: "carol".to_string(), 1397 amount: 8, 1398 eligible_from_height: height, 1399 eligible_until_height: height + 2, 1400 }, 1401 BurnTicket { 1402 id: "future-same-owner".to_string(), 1403 owner: "alice".to_string(), 1404 amount: 5, 1405 eligible_from_height: height + 1, 1406 eligible_until_height: height + 3, 1407 }, 1408 ] 1409 } 1410 1411 #[test] 1412 fn primary_ticket_at_activation_height_only_consumes_winning_ticket() { 1413 let height = MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT; 1414 let parent = ticket_test_block(height - 1, FinalizerMode::Ticket, 0, "primary-parent"); 1415 let mut tickets = fallback_invalidation_tickets(height); 1416 let ranked = ranked_tickets_for_height(&parent, height, &tickets); 1417 let leader = &ranked[0]; 1418 let leader_id = leader.id.clone(); 1419 let leader_owner = leader.owner.clone(); 1420 let eligible_before = tickets 1421 .iter() 1422 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1423 .count(); 1424 let block = ticket_test_child(&parent, height, &leader_owner, 0, &leader_id); 1425 1426 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1427 1428 assert!(tickets.iter().all(|ticket| ticket.id != leader_id)); 1429 let eligible_after = tickets 1430 .iter() 1431 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1432 .count(); 1433 assert_eq!(eligible_after, eligible_before - 1); 1434 } 1435 1436 #[test] 1437 fn fallback_ticket_before_activation_height_only_consumes_winning_ticket() { 1438 let height = MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT - 1; 1439 let parent = ticket_test_block( 1440 height - 1, 1441 FinalizerMode::Ticket, 1442 0, 1443 "legacy-fallback-parent", 1444 ); 1445 let mut tickets = fallback_invalidation_tickets(height); 1446 let ranked = ranked_tickets_for_height(&parent, height, &tickets); 1447 let fallback = &ranked[1]; 1448 let fallback_id = fallback.id.clone(); 1449 let fallback_owner = fallback.owner.clone(); 1450 let eligible_before = tickets 1451 .iter() 1452 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1453 .count(); 1454 let block = ticket_test_child(&parent, height, &fallback_owner, 1, &fallback_id); 1455 1456 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1457 1458 assert!(tickets.iter().all(|ticket| ticket.id != fallback_id)); 1459 let eligible_after = tickets 1460 .iter() 1461 .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) 1462 .count(); 1463 assert_eq!(eligible_after, eligible_before - 1); 1464 } 1465 1466 #[test] 1467 fn fallback_ticket_invalidates_missed_ranks_and_current_sibling_tickets_from_height_300() { 1468 let height = MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT; 1469 let parent = ticket_test_block(height - 1, FinalizerMode::Ticket, 0, "fallback-parent"); 1470 let mut tickets = fallback_invalidation_tickets(height); 1471 let ranked = ranked_tickets_for_height(&parent, height, &tickets); 1472 let prefix_owners = ranked 1473 .iter() 1474 .take(2) 1475 .map(|ticket| ticket.owner.clone()) 1476 .collect::<BTreeSet<_>>(); 1477 let fallback = &ranked[1]; 1478 let block = ticket_test_child(&parent, height, &fallback.owner, 1, &fallback.id); 1479 1480 consume_leader_ticket(&parent, &block, &mut tickets).unwrap(); 1481 1482 assert!( 1483 tickets.iter().all(|ticket| { 1484 !ticket_is_eligible_for_height(ticket, height) || !prefix_owners.contains(&ticket.owner) 1485 }), 1486 "eligible tickets from missed ranks and the fallback finalizer should be invalidated" 1487 ); 1488 assert!( 1489 tickets 1490 .iter() 1491 .any(|ticket| ticket.id == "future-same-owner"), 1492 "future tickets from invalidated owners should stay pending" 1493 ); 1494 } 1495 1496 #[test] 1497 fn burn_leader_ranks_for_block_reconstructs_historical_ticket_order() { 1498 let alice = Wallet::from_seed("burn-rank-alice"); 1499 let bob = Wallet::from_seed("burn-rank-bob"); 1500 let mut allocations = BTreeMap::new(); 1501 allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); 1502 allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); 1503 let ledger = Ledger::new_with_genesis_burns( 1504 allocations, 1505 vec![ 1506 GenesisBurn::new(alice.address(), MICRO_IUNA), 1507 GenesisBurn::new(bob.address(), MICRO_IUNA), 1508 ], 1509 1, 1510 ) 1511 .unwrap(); 1512 1513 let ranks = ledger.burn_leader_ranks_for_block(1).unwrap(); 1514 let leader = ledger.expected_leader_for_next_block().unwrap(); 1515 1516 assert_eq!(ranks.len(), 2); 1517 assert_eq!(ranks[0].rank, 0); 1518 assert_eq!(ranks[0].owner, leader); 1519 assert!(ranks.iter().all(|rank| rank.amount == MICRO_IUNA)); 1520 assert_eq!(ledger.burn_leader_ranks_for_block(0).unwrap(), Vec::new()); 1521 1522 let batch = ledger.burn_leader_ranks_for_blocks([0, 1]).unwrap(); 1523 assert!(batch.get(&0).unwrap().is_empty()); 1524 assert_eq!(batch.get(&1), Some(&ranks)); 1525 } 1526 1527 #[test] 1528 fn mine_recipient_is_bound_to_proof_hash() { 1529 let alice = Wallet::from_seed("mine-proof-alice"); 1530 let bob = Wallet::from_seed("mine-proof-bob"); 1531 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1532 let mut forged = unsigned_mine(&ledger, alice.address()); 1533 if let Transaction::Mine { recipient, .. } = &mut forged { 1534 *recipient = bob.address().to_string(); 1535 } 1536 1537 let error = ledger.submit_transaction(forged).unwrap_err(); 1538 1539 assert!(format!("{error:#}").contains("proof hash is invalid")); 1540 } 1541 1542 #[test] 1543 fn mine_action_uses_fixed_reward_and_fixed_finalizer_fee() { 1544 let alice = Wallet::from_seed("mine-fixed-reward-alice"); 1545 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1546 1547 let mine = ledger.build_mine(alice.address()).unwrap(); 1548 1549 assert_eq!(mine.amount(), MINE_REWARD); 1550 assert_eq!(mine.fee(), MINE_FINALIZER_FEE); 1551 assert!(ledger.submit_transaction(mine).unwrap()); 1552 } 1553 1554 #[test] 1555 fn burn_fee_goes_to_block_finalizer() { 1556 let alice = Wallet::from_seed("burn-fee-finalizer-alice"); 1557 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1558 1559 let burn_fee = 12; 1560 let burn = ledger 1561 .build_burn(&alice, TEST_BURN_AMOUNT, burn_fee) 1562 .unwrap(); 1563 ledger.submit_transaction(burn).unwrap(); 1564 let prepared = ledger.prepare_next_block(alice.address(), 1).unwrap(); 1565 1566 assert_eq!(prepared.reward, burn_fee); 1567 } 1568 1569 #[test] 1570 fn blinded_burn_commits_ciphertext_and_reveal_executes_later() { 1571 let alice = Wallet::from_seed("blinded-burn-finalizer-alice"); 1572 let bob = Wallet::from_seed("blinded-burn-finalizer-bob"); 1573 let carol = Wallet::from_seed("blinded-burn-carol"); 1574 let finalizers = [alice.clone(), bob.clone()]; 1575 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1576 let fee = 100; 1577 let burn_amount = 3; 1578 let before_carol = ledger.balance_of(carol.address()); 1579 1580 let blinded = ledger 1581 .build_blinded_burn(&carol, burn_amount, fee, ledger.height() + 4) 1582 .unwrap(); 1583 assert!(!blinded.transaction.ciphertext.contains("burn")); 1584 assert!(!blinded.transaction.ciphertext.contains(carol.address())); 1585 ledger 1586 .submit_blinded_transaction(blinded.transaction.clone()) 1587 .unwrap(); 1588 queue_next_leader_burn(&mut ledger, &finalizers); 1589 1590 let commit_block = mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1591 let inclusion_finalizer = commit_block.miner.clone(); 1592 assert_eq!( 1593 commit_block 1594 .transactions 1595 .iter() 1596 .filter(|transaction| transaction.is_burn()) 1597 .count(), 1598 1 1599 ); 1600 assert_eq!(commit_block.blinded_transactions, vec![blinded.transaction]); 1601 assert_eq!(commit_block.reward, 0); 1602 let before_inclusion_finalizer = ledger.balance_of(&inclusion_finalizer); 1603 1604 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1605 queue_next_leader_burn(&mut ledger, &finalizers); 1606 let reveal_block = 1607 mine_preverified_as_next_leader_with_reveal_bundles(&mut ledger, &finalizers, 2); 1608 let reveal_executor = reveal_block.miner.clone(); 1609 1610 assert_eq!(reveal_block.all_blinded_reveals().len(), 1); 1611 assert_eq!( 1612 ledger.balance_of(carol.address()), 1613 before_carol - burn_amount - fee 1614 ); 1615 assert!(ledger.tickets.iter().any(|ticket| { 1616 ticket.owner == carol.address() 1617 && ticket.amount == burn_amount 1618 && ticket.eligible_from_height 1619 == reveal_block.height + ledger.launch_profile.ticket_maturity_delay_heights 1620 })); 1621 let reveal_plaintext_burn_spent_by_inclusion_finalizer = reveal_block 1622 .transactions 1623 .iter() 1624 .filter(|transaction| { 1625 transaction.is_burn() && transaction.sender() == inclusion_finalizer.as_str() 1626 }) 1627 .fold(0_u64, |total, transaction| { 1628 total + transaction.amount() + transaction.fee() 1629 }); 1630 let committer_fee = blinded_fee_share(fee, BLINDED_COMMITTER_FEE_BPS); 1631 let reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1632 fee, 1633 reveal_block.included_reveal_bundle_count(), 1634 ledger 1635 .burn_leader_ranks_for_block(reveal_block.height) 1636 .unwrap() 1637 .len(), 1638 ); 1639 let reveal_bundle_signer_fee = blinded_fee_share(fee, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS); 1640 let commitment = &commit_block.blinded_transactions[0].commitment; 1641 assert_eq!( 1642 ledger 1643 .utxos 1644 .get(&blinded_committer_fee_outpoint(commitment)) 1645 .unwrap(), 1646 &TxOutput { 1647 address: inclusion_finalizer.clone(), 1648 amount: committer_fee, 1649 } 1650 ); 1651 assert!( 1652 !ledger 1653 .utxos 1654 .contains_key(&blinded_executor_fee_outpoint(commitment)) 1655 ); 1656 assert_eq!(reveal_block.reward, reveal_finalizer_fee); 1657 assert_eq!( 1658 ledger 1659 .utxos 1660 .get(&reward_outpoint(&reveal_block.hash)) 1661 .unwrap(), 1662 &TxOutput { 1663 address: reveal_executor.clone(), 1664 amount: reveal_finalizer_fee, 1665 } 1666 ); 1667 for signature in &reveal_block.reveal_bundle_section.signatures { 1668 assert_eq!( 1669 ledger 1670 .utxos 1671 .get(&blinded_reveal_bundle_signer_fee_outpoint( 1672 commitment, 1673 signature.slot 1674 )) 1675 .unwrap(), 1676 &TxOutput { 1677 address: signature.member.clone(), 1678 amount: reveal_bundle_signer_fee, 1679 } 1680 ); 1681 } 1682 let mut inclusion_finalizer_fee = committer_fee; 1683 if inclusion_finalizer == reveal_executor { 1684 inclusion_finalizer_fee += reveal_finalizer_fee; 1685 } 1686 inclusion_finalizer_fee += reveal_block 1687 .reveal_bundle_section 1688 .signatures 1689 .iter() 1690 .filter(|signature| signature.member == inclusion_finalizer) 1691 .count() as u64 1692 * reveal_bundle_signer_fee; 1693 assert_eq!( 1694 ledger.balance_of(&inclusion_finalizer), 1695 before_inclusion_finalizer + inclusion_finalizer_fee 1696 - reveal_plaintext_burn_spent_by_inclusion_finalizer 1697 ); 1698 } 1699 1700 #[test] 1701 fn blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { 1702 let alice = Wallet::from_seed("aggregated-finalizer-fee-alice"); 1703 let bob = Wallet::from_seed("aggregated-finalizer-fee-bob"); 1704 let carol = Wallet::from_seed("aggregated-finalizer-fee-carol"); 1705 let dave = Wallet::from_seed("aggregated-finalizer-fee-dave"); 1706 let finalizers = [alice.clone(), bob.clone()]; 1707 let mut ledger = ledger_with_finalizers( 1708 &finalizers, 1709 &[(&carol, 10 * MICRO_IUNA), (&dave, 10 * MICRO_IUNA)], 1710 ); 1711 let first_fee = 100; 1712 let second_fee = 200; 1713 let first_blinded = ledger 1714 .build_blinded_burn(&carol, 3, first_fee, ledger.height() + 4) 1715 .unwrap(); 1716 let second_blinded = ledger 1717 .build_blinded_burn(&dave, 4, second_fee, ledger.height() + 4) 1718 .unwrap(); 1719 let first_commitment = first_blinded.transaction.commitment.clone(); 1720 let second_commitment = second_blinded.transaction.commitment.clone(); 1721 ledger 1722 .submit_blinded_transaction(first_blinded.transaction) 1723 .unwrap(); 1724 ledger 1725 .submit_blinded_transaction(second_blinded.transaction) 1726 .unwrap(); 1727 queue_next_leader_burn(&mut ledger, &finalizers); 1728 let commit_timestamp_ms = ledger 1729 .tip() 1730 .timestamp_ms 1731 .saturating_add(VDF_TARGET_BLOCK_MS); 1732 let commit_block = 1733 mine_preverified_as_next_leader(&mut ledger, &finalizers, commit_timestamp_ms); 1734 assert_eq!(commit_block.height, 1); 1735 1736 ledger.submit_blinded_reveal(first_blinded.reveal).unwrap(); 1737 ledger.submit_blinded_reveal(second_blinded.reveal).unwrap(); 1738 queue_next_leader_burn(&mut ledger, &finalizers); 1739 let reveal_timestamp_ms = ledger 1740 .tip() 1741 .timestamp_ms 1742 .saturating_add(VDF_TARGET_BLOCK_MS); 1743 let reveal_block = prepare_preverified_as_next_leader_with_reveal_bundles( 1744 &ledger, 1745 &finalizers, 1746 reveal_timestamp_ms, 1747 ); 1748 let first_reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1749 first_fee, 1750 reveal_block.included_reveal_bundle_count(), 1751 ledger 1752 .burn_leader_ranks_for_block(reveal_block.height) 1753 .unwrap() 1754 .len(), 1755 ); 1756 let second_reveal_finalizer_fee = blinded_reveal_finalizer_fee( 1757 second_fee, 1758 reveal_block.included_reveal_bundle_count(), 1759 ledger 1760 .burn_leader_ranks_for_block(reveal_block.height) 1761 .unwrap() 1762 .len(), 1763 ); 1764 let aggregate_reveal_finalizer_fee = first_reveal_finalizer_fee 1765 .checked_add(second_reveal_finalizer_fee) 1766 .unwrap(); 1767 1768 assert_eq!(reveal_block.height, 2); 1769 assert_eq!(reveal_block.reward, aggregate_reveal_finalizer_fee); 1770 let mut plain_fee_reward_block = reveal_block.clone(); 1771 plain_fee_reward_block.reward = fee_reward(&plain_fee_reward_block.transactions).unwrap(); 1772 plain_fee_reward_block.hash = plain_fee_reward_block.compute_hash(); 1773 let error = ledger 1774 .clone() 1775 .apply_preverified_block_at(plain_fee_reward_block, u64::MAX) 1776 .unwrap_err(); 1777 assert!(format!("{error:#}").contains("block reward is invalid")); 1778 1779 ledger 1780 .apply_preverified_block_at(reveal_block.clone(), u64::MAX) 1781 .unwrap(); 1782 assert!( 1783 !ledger 1784 .utxos 1785 .contains_key(&blinded_executor_fee_outpoint(&first_commitment)) 1786 ); 1787 assert!( 1788 !ledger 1789 .utxos 1790 .contains_key(&blinded_executor_fee_outpoint(&second_commitment)) 1791 ); 1792 assert_eq!( 1793 ledger.utxos.get(&reward_outpoint(&reveal_block.hash)), 1794 Some(&TxOutput { 1795 address: reveal_block.miner.clone(), 1796 amount: aggregate_reveal_finalizer_fee, 1797 }) 1798 ); 1799 } 1800 1801 #[test] 1802 fn blinded_utxo_commit_exposes_and_locks_inputs_until_reveal_or_expiry() { 1803 let alice = Wallet::from_seed("blinded-lock-alice"); 1804 let bob = Wallet::from_seed("blinded-lock-bob"); 1805 let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); 1806 let transfer = ledger.build_transfer(&alice, bob.address(), 3, 2).unwrap(); 1807 let visible_inputs = transfer.inputs().to_vec(); 1808 1809 let blinded = ledger 1810 .build_blinded_transaction(&alice, transfer, ledger.height() + 4) 1811 .unwrap(); 1812 1813 assert_eq!(blinded.transaction.inputs.len(), visible_inputs.len()); 1814 assert_eq!( 1815 unsigned_inputs(&blinded.transaction.inputs), 1816 unsigned_inputs(&visible_inputs) 1817 ); 1818 ledger 1819 .submit_blinded_transaction(blinded.transaction) 1820 .unwrap(); 1821 let error = ledger 1822 .build_transfer(&alice, bob.address(), 1, 0) 1823 .unwrap_err(); 1824 assert!(format!("{error:#}").contains("insufficient funds")); 1825 } 1826 1827 #[test] 1828 fn blinded_payload_omits_visible_inputs_and_reconstructs_transaction_on_reveal() { 1829 let alice = Wallet::from_seed("blinded-compact-payload-alice"); 1830 let bob = Wallet::from_seed("blinded-compact-payload-bob"); 1831 let ledger = ledger_with_wallet_utxos(&alice, &[10]); 1832 let transfer = ledger.build_transfer(&alice, bob.address(), 3, 2).unwrap(); 1833 let full_transaction_bytes = serde_json::to_vec(&transfer).unwrap().len(); 1834 let blinded = ledger 1835 .build_blinded_transaction(&alice, transfer.clone(), ledger.height() + 4) 1836 .unwrap(); 1837 let key = decode_hex_array::<BLINDED_KEY_BYTES>(&blinded.reveal.key).unwrap(); 1838 let nonce = decode_hex_array::<BLINDED_NONCE_BYTES>(&blinded.transaction.nonce).unwrap(); 1839 let ciphertext = decode_hex(&blinded.transaction.ciphertext).unwrap(); 1840 1841 let plaintext = decrypt_blinded_payload( 1842 &key, 1843 &nonce, 1844 &signed_blinded_inputs(&unsigned_inputs(&blinded.transaction.inputs), ""), 1845 blinded.transaction.fee, 1846 blinded.transaction.expires_at_height, 1847 &ciphertext, 1848 ) 1849 .unwrap(); 1850 let payload: serde_json::Value = serde_json::from_slice(&plaintext).unwrap(); 1851 let revealed = decrypt_blinded_transaction(&blinded.transaction, &blinded.reveal).unwrap(); 1852 1853 assert_eq!( 1854 payload.get("kind").and_then(|kind| kind.as_str()), 1855 Some("transfer") 1856 ); 1857 assert!(payload.get("inputs").is_none()); 1858 assert!(plaintext.len() < full_transaction_bytes); 1859 assert_eq!(revealed, transfer); 1860 } 1861 1862 #[test] 1863 fn unrevealed_blinded_utxo_commit_burns_fee_and_returns_change() { 1864 let alice = Wallet::from_seed("blinded-expiry-alice"); 1865 let bob = Wallet::from_seed("blinded-expiry-bob"); 1866 let carol = Wallet::from_seed("blinded-expiry-carol"); 1867 let finalizers = [alice.clone(), bob.clone()]; 1868 let carol_balance = 10 * MICRO_IUNA; 1869 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, carol_balance)]); 1870 let fee = 100; 1871 let blinded = ledger 1872 .build_blinded_burn(&carol, 3, fee, ledger.height() + 2) 1873 .unwrap(); 1874 let commitment = blinded.transaction.commitment.clone(); 1875 ledger 1876 .submit_blinded_transaction(blinded.transaction) 1877 .unwrap(); 1878 queue_next_leader_burn(&mut ledger, &finalizers); 1879 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1880 1881 assert_eq!(ledger.balance_of(carol.address()), 0); 1882 queue_next_leader_burn(&mut ledger, &finalizers); 1883 mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); 1884 1885 assert_eq!( 1886 ledger 1887 .utxos 1888 .get(&blinded_committer_fee_outpoint(&commitment)), 1889 None 1890 ); 1891 assert_eq!( 1892 ledger 1893 .utxos 1894 .get(&blinded_expiry_change_outpoint(&commitment)), 1895 Some(&TxOutput { 1896 address: carol.address().to_string(), 1897 amount: carol_balance - fee, 1898 }) 1899 ); 1900 assert_eq!(ledger.balance_of(carol.address()), carol_balance - fee); 1901 } 1902 1903 #[test] 1904 fn fee_bearing_blinded_commit_without_inputs_is_rejected() { 1905 let alice = Wallet::from_seed("blinded-no-input-fee-alice"); 1906 let mut ledger = ledger_with_allocation(&alice, MICRO_IUNA); 1907 let mut blinded = ledger 1908 .build_blinded_burn(&alice, 1, 1, ledger.height() + 4) 1909 .unwrap() 1910 .transaction; 1911 blinded.inputs.clear(); 1912 blinded.commitment = blinded_transaction_commitment(&blinded).unwrap(); 1913 1914 let error = ledger.submit_blinded_transaction(blinded).unwrap_err(); 1915 1916 assert!(format!("{error:#}").contains("must lock visible inputs")); 1917 } 1918 1919 #[test] 1920 fn mine_actions_cannot_be_blinded() { 1921 let alice = Wallet::from_seed("blinded-mine-collateral-alice"); 1922 let ledger = ledger_with_finalizers(&[alice.clone()], &[]); 1923 let mine = ledger.build_mine(alice.address()).unwrap(); 1924 let error = ledger 1925 .build_blinded_transaction(&alice, mine, ledger.height() + 4) 1926 .unwrap_err(); 1927 1928 assert!(format!("{error:#}").contains("mine actions are public")); 1929 } 1930 1931 #[test] 1932 fn reveal_bundle_hashes_are_bound_to_next_block_vdf_seed() { 1933 let alice = Wallet::from_seed("bundle-seed-alice"); 1934 let bob = Wallet::from_seed("bundle-seed-bob"); 1935 let carol = Wallet::from_seed("bundle-seed-carol"); 1936 let finalizers = [alice.clone(), bob.clone()]; 1937 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 1938 let blinded = ledger 1939 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 1940 .unwrap(); 1941 ledger 1942 .submit_blinded_transaction(blinded.transaction.clone()) 1943 .unwrap(); 1944 queue_next_leader_burn(&mut ledger, &finalizers); 1945 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1946 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 1947 queue_next_leader_burn(&mut ledger, &finalizers); 1948 let leader = ledger.expected_leader_for_next_block().unwrap(); 1949 let leader_wallet = wallet_for_address(&finalizers, &leader); 1950 let bundles = ledger 1951 .reveal_committee_for_next_block() 1952 .into_iter() 1953 .filter_map(|member| { 1954 let wallet = wallet_for_address(&finalizers, &member.owner); 1955 ledger.build_reveal_bundle(wallet).unwrap() 1956 }) 1957 .collect::<Vec<_>>(); 1958 if bundles.len() > 1 { 1959 let mut reversed = bundles.clone(); 1960 reversed.reverse(); 1961 let error = ledger 1962 .validate_next_block_reveal_bundles(reversed) 1963 .unwrap_err(); 1964 assert!(format!("{error:#}").contains("reveal bundles are not in slot order")); 1965 } 1966 1967 let without_bundles = ledger 1968 .prepare_next_block(leader_wallet.address(), ledger.tip().timestamp_ms + 1) 1969 .unwrap(); 1970 let with_bundles = ledger 1971 .prepare_next_block_with_reveal_bundles( 1972 leader_wallet.address(), 1973 ledger.tip().timestamp_ms + 1, 1974 bundles, 1975 ) 1976 .unwrap(); 1977 1978 assert_ne!(without_bundles.vdf_seed(), with_bundles.vdf_seed()); 1979 } 1980 1981 #[test] 1982 fn reveal_committee_includes_next_block_finalizer_as_slot_zero() { 1983 let alice = Wallet::from_seed("bundle-finalizer-slot-alice"); 1984 let bob = Wallet::from_seed("bundle-finalizer-slot-bob"); 1985 let carol = Wallet::from_seed("bundle-finalizer-slot-carol"); 1986 let dave = Wallet::from_seed("bundle-finalizer-slot-dave"); 1987 let erin = Wallet::from_seed("bundle-finalizer-slot-erin"); 1988 let finalizers = [alice.clone(), bob.clone(), carol.clone(), dave.clone()]; 1989 let mut ledger = ledger_with_finalizers(&finalizers, &[(&erin, 10 * MICRO_IUNA)]); 1990 let blinded = ledger 1991 .build_blinded_burn(&erin, 3, 7, ledger.height() + 4) 1992 .unwrap(); 1993 let commitment = blinded.transaction.commitment.clone(); 1994 ledger 1995 .submit_blinded_transaction(blinded.transaction) 1996 .unwrap(); 1997 queue_next_leader_burn(&mut ledger, &finalizers); 1998 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 1999 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2000 queue_next_leader_burn(&mut ledger, &finalizers); 2001 2002 let leader = ledger.expected_leader_for_next_block().unwrap(); 2003 let committee = ledger.reveal_committee_for_next_block(); 2004 let leader_wallet = wallet_for_address(&finalizers, &leader); 2005 let bundle = ledger.build_reveal_bundle(leader_wallet).unwrap().unwrap(); 2006 2007 assert_eq!(committee.first().map(|member| member.slot), Some(0)); 2008 assert_eq!(committee.first().map(|member| member.rank), Some(0)); 2009 assert_eq!( 2010 committee.first().map(|member| member.owner.as_str()), 2011 Some(leader.as_str()) 2012 ); 2013 assert_eq!(bundle.slot, 0); 2014 assert_eq!(bundle.member, leader); 2015 assert!( 2016 bundle 2017 .reveals 2018 .iter() 2019 .any(|reveal| reveal.commitment == commitment) 2020 ); 2021 } 2022 2023 #[test] 2024 fn reveal_bundle_section_deduplicates_reveals_with_slot_mask() { 2025 let alice = Wallet::from_seed("bundle-compact-alice"); 2026 let bob = Wallet::from_seed("bundle-compact-bob"); 2027 let carol = Wallet::from_seed("bundle-compact-carol"); 2028 let dave = Wallet::from_seed("bundle-compact-dave"); 2029 let finalizers = [alice.clone(), bob.clone(), carol.clone()]; 2030 let mut ledger = ledger_with_finalizers(&finalizers, &[(&dave, 10 * MICRO_IUNA)]); 2031 let blinded = ledger 2032 .build_blinded_burn(&dave, 3, 7, ledger.height() + 4) 2033 .unwrap(); 2034 ledger 2035 .submit_blinded_transaction(blinded.transaction.clone()) 2036 .unwrap(); 2037 queue_next_leader_burn(&mut ledger, &finalizers); 2038 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2039 ledger 2040 .submit_blinded_reveal(blinded.reveal.clone()) 2041 .unwrap(); 2042 queue_next_leader_burn(&mut ledger, &finalizers); 2043 2044 let mut bundles = ledger 2045 .reveal_committee_for_next_block() 2046 .into_iter() 2047 .filter_map(|member| { 2048 let wallet = wallet_for_address(&finalizers, &member.owner); 2049 ledger.build_reveal_bundle(wallet).unwrap() 2050 }) 2051 .collect::<Vec<_>>(); 2052 assert!(bundles.len() >= 2); 2053 bundles.truncate(2); 2054 let expected_hashes = reveal_bundle_hashes(&bundles); 2055 let expected_mask = bundles 2056 .iter() 2057 .fold(0_u8, |mask, bundle| mask | (1_u8 << bundle.slot)); 2058 2059 let leader = ledger.expected_leader_for_next_block().unwrap(); 2060 let leader_wallet = wallet_for_address(&finalizers, &leader); 2061 let prepared = ledger 2062 .prepare_next_block_with_reveal_bundles( 2063 leader_wallet.address(), 2064 ledger.tip().timestamp_ms + 1, 2065 bundles.clone(), 2066 ) 2067 .unwrap(); 2068 let block = prepared.finish(leader_wallet, "preverified-vdf".to_string()); 2069 2070 assert_eq!(block.reveal_bundle_section.signatures.len(), 2); 2071 assert_eq!(block.reveal_bundle_section.reveals.len(), 1); 2072 assert_eq!( 2073 block.reveal_bundle_section.reveals[0].bundle_mask, 2074 expected_mask 2075 ); 2076 assert_eq!(block.all_blinded_reveals(), vec![&blinded.reveal]); 2077 assert_eq!(block.reveal_bundle_hashes(), expected_hashes); 2078 assert_eq!( 2079 block 2080 .reveal_bundle_section 2081 .expand(block.height, &block.prev_hash), 2082 bundles 2083 ); 2084 } 2085 2086 #[test] 2087 fn reveal_bundle_validation_rejects_wrong_signature_and_slot() { 2088 let alice = Wallet::from_seed("bundle-invalid-alice"); 2089 let bob = Wallet::from_seed("bundle-invalid-bob"); 2090 let carol = Wallet::from_seed("bundle-invalid-carol"); 2091 let finalizers = [alice.clone(), bob.clone()]; 2092 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2093 let blinded = ledger 2094 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2095 .unwrap(); 2096 ledger 2097 .submit_blinded_transaction(blinded.transaction.clone()) 2098 .unwrap(); 2099 queue_next_leader_burn(&mut ledger, &finalizers); 2100 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2101 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2102 let member = ledger.reveal_committee_for_next_block()[0].clone(); 2103 let wallet = wallet_for_address(&finalizers, &member.owner); 2104 let bundle = ledger.build_reveal_bundle(wallet).unwrap().unwrap(); 2105 2106 let mut wrong_signature = bundle.clone(); 2107 wrong_signature.signature = "00".repeat(SIGNATURE_BYTES); 2108 let error = ledger 2109 .validate_next_block_reveal_bundles(vec![wrong_signature]) 2110 .unwrap_err(); 2111 assert!(format!("{error:#}").contains("reveal bundle signature is invalid")); 2112 2113 let mut wrong_slot = bundle; 2114 wrong_slot.slot = REVEAL_COMMITTEE_SIZE as u8 - 1; 2115 let error = ledger 2116 .validate_next_block_reveal_bundles(vec![wrong_slot]) 2117 .unwrap_err(); 2118 assert!( 2119 format!("{error:#}").contains("reveal bundle slot is not assigned") 2120 || format!("{error:#}").contains("reveal bundle member is not assigned to slot") 2121 ); 2122 } 2123 2124 #[test] 2125 fn blinded_reveal_with_wrong_key_is_rejected_in_block() { 2126 let alice = Wallet::from_seed("blinded-wrong-key-finalizer-alice"); 2127 let bob = Wallet::from_seed("blinded-wrong-key-finalizer-bob"); 2128 let carol = Wallet::from_seed("blinded-wrong-key-carol"); 2129 let finalizers = [alice.clone(), bob.clone()]; 2130 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2131 let blinded = ledger 2132 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2133 .unwrap(); 2134 ledger 2135 .submit_blinded_transaction(blinded.transaction.clone()) 2136 .unwrap(); 2137 queue_next_leader_burn(&mut ledger, &finalizers); 2138 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2139 2140 let leader = ledger.expected_leader_for_next_block().unwrap(); 2141 let wallet = wallet_for_address(&finalizers, &leader); 2142 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2143 ledger.submit_transaction(filler_burn).unwrap(); 2144 let mut prepared = ledger 2145 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 2146 .unwrap(); 2147 let committee_member = ledger.reveal_committee_for_next_block()[0].clone(); 2148 let committee_wallet = wallet_for_address(&finalizers, &committee_member.owner); 2149 let wrong_reveal = BlindedReveal { 2150 commitment: blinded.transaction.commitment, 2151 key: "00".repeat(BLINDED_KEY_BYTES), 2152 }; 2153 let wrong_bundle = committee_wallet.reveal_bundle(RevealBundlePayload { 2154 height: prepared.height, 2155 prev_hash: prepared.prev_hash.clone(), 2156 slot: committee_member.slot, 2157 member: committee_wallet.address().to_string(), 2158 reveals: vec![wrong_reveal], 2159 }); 2160 prepared.reveal_bundle_section = ledger.reveal_bundle_section_from_bundles(vec![wrong_bundle]); 2161 prepared.reward = blinded_reveal_finalizer_fee( 2162 blinded.transaction.fee, 2163 prepared.reveal_bundle_section.signatures.len(), 2164 ledger.reveal_committee_for_next_block().len(), 2165 ); 2166 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 2167 2168 let error = ledger 2169 .apply_preverified_block_at(block, u64::MAX) 2170 .unwrap_err(); 2171 2172 assert!( 2173 format!("{error:#}").contains("decrypt blinded transaction"), 2174 "{error:#}" 2175 ); 2176 } 2177 2178 #[test] 2179 fn expired_blinded_reveal_is_not_selected() { 2180 let alice = Wallet::from_seed("blinded-expire-finalizer-alice"); 2181 let bob = Wallet::from_seed("blinded-expire-finalizer-bob"); 2182 let carol = Wallet::from_seed("blinded-expire-carol"); 2183 let finalizers = [alice.clone(), bob.clone()]; 2184 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2185 let blinded = ledger 2186 .build_blinded_burn(&carol, 3, 7, ledger.height() + 2) 2187 .unwrap(); 2188 ledger 2189 .submit_blinded_transaction(blinded.transaction.clone()) 2190 .unwrap(); 2191 queue_next_leader_burn(&mut ledger, &finalizers); 2192 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2193 2194 let leader = ledger.expected_leader_for_next_block().unwrap(); 2195 let wallet = wallet_for_address(&finalizers, &leader); 2196 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2197 ledger.submit_transaction(filler_burn).unwrap(); 2198 mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); 2199 2200 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2201 assert!(ledger.valid_pending_blinded_reveals().is_empty()); 2202 } 2203 2204 #[test] 2205 fn recovery_block_includes_pending_blinded_transactions_when_space_allows() { 2206 let alice = Wallet::from_seed("recovery-blinded-commit-alice"); 2207 let bob = Wallet::from_seed("recovery-blinded-commit-bob"); 2208 let carol = Wallet::from_seed("recovery-blinded-commit-carol"); 2209 let mut ledger = ledger_with_finalizers( 2210 &[alice], 2211 &[(&bob, 10 * MICRO_IUNA), (&carol, 10 * MICRO_IUNA)], 2212 ); 2213 let blinded = ledger 2214 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2215 .unwrap(); 2216 ledger 2217 .submit_blinded_transaction(blinded.transaction.clone()) 2218 .unwrap(); 2219 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2220 ledger.submit_transaction(recovery_burn).unwrap(); 2221 2222 let block = ledger 2223 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 2224 .unwrap(); 2225 2226 assert!( 2227 block 2228 .blinded_transactions 2229 .iter() 2230 .any(|transaction| transaction.commitment == blinded.transaction.commitment) 2231 ); 2232 } 2233 2234 #[test] 2235 fn recovery_block_size_selection_uses_recovery_skeleton() { 2236 let alice = Wallet::from_seed("recovery-size-commit-alice"); 2237 let bob = Wallet::from_seed("recovery-size-commit-bob"); 2238 let carol = Wallet::from_seed("recovery-size-commit-carol"); 2239 let mut ledger = ledger_with_finalizers( 2240 &[alice], 2241 &[(&bob, 10 * MICRO_IUNA), (&carol, 10 * MICRO_IUNA)], 2242 ); 2243 let blinded = ledger 2244 .build_blinded_burn(&carol, MICRO_IUNA, 7, ledger.height() + 4) 2245 .unwrap(); 2246 ledger 2247 .submit_blinded_transaction(blinded.transaction.clone()) 2248 .unwrap(); 2249 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2250 ledger.submit_transaction(recovery_burn.clone()).unwrap(); 2251 let recovery_selection = BlockSelection { 2252 transactions: vec![recovery_burn], 2253 blinded_transactions: vec![blinded.transaction.clone()], 2254 }; 2255 let recovery_estimate = 2256 estimated_block_selection_size_bytes(&recovery_selection, true).unwrap(); 2257 let ticket_estimate = estimated_block_selection_size_bytes(&recovery_selection, false).unwrap(); 2258 assert!(recovery_estimate < ticket_estimate); 2259 2260 ledger.launch_profile.max_block_bytes = recovery_estimate; 2261 let tight_block = ledger 2262 .mine_recovery_block(&bob, RECOVERY_BLOCK_DELAY_MS) 2263 .unwrap(); 2264 2265 assert!( 2266 tight_block 2267 .blinded_transactions 2268 .iter() 2269 .any(|transaction| transaction.commitment == blinded.transaction.commitment) 2270 ); 2271 } 2272 2273 #[test] 2274 fn recovery_block_includes_pending_blinded_reveals_when_space_allows() { 2275 let alice = Wallet::from_seed("recovery-blinded-reveal-alice"); 2276 let bob = Wallet::from_seed("recovery-blinded-reveal-bob"); 2277 let carol = Wallet::from_seed("recovery-blinded-reveal-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, MICRO_IUNA, 7, ledger.height() + 4) 2282 .unwrap(); 2283 ledger 2284 .submit_blinded_transaction(blinded.transaction.clone()) 2285 .unwrap(); 2286 queue_next_leader_burn(&mut ledger, &finalizers); 2287 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2288 ledger 2289 .submit_blinded_reveal(blinded.reveal.clone()) 2290 .unwrap(); 2291 let recovery_burn = ledger.build_burn(&bob, MICRO_IUNA, 0).unwrap(); 2292 ledger.submit_transaction(recovery_burn).unwrap(); 2293 2294 let bundles = ledger 2295 .reveal_committee_for_next_block() 2296 .into_iter() 2297 .filter_map(|member| { 2298 let wallet = wallet_for_address(&finalizers, &member.owner); 2299 ledger.build_reveal_bundle(wallet).unwrap() 2300 }) 2301 .collect::<Vec<_>>(); 2302 let prepared = ledger 2303 .prepare_recovery_block_with_reveal_bundles( 2304 bob.address(), 2305 ledger.recovery_block_min_timestamp(), 2306 bundles, 2307 ) 2308 .unwrap(); 2309 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 2310 let block = prepared.finish(&bob, vdf_output); 2311 2312 assert!( 2313 block 2314 .all_blinded_reveals() 2315 .iter() 2316 .any(|reveal| reveal.commitment == blinded.transaction.commitment) 2317 ); 2318 } 2319 2320 #[test] 2321 fn blinded_transaction_expiring_at_next_height_is_not_selected() { 2322 let alice = Wallet::from_seed("blinded-next-expire-finalizer-alice"); 2323 let bob = Wallet::from_seed("blinded-next-expire-finalizer-bob"); 2324 let carol = Wallet::from_seed("blinded-next-expire-carol"); 2325 let finalizers = [alice.clone(), bob.clone()]; 2326 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2327 let blinded = ledger 2328 .build_blinded_burn(&carol, 3, 7, ledger.height() + 1) 2329 .unwrap(); 2330 ledger 2331 .submit_blinded_transaction(blinded.transaction) 2332 .unwrap(); 2333 2334 let leader = ledger.expected_leader_for_next_block().unwrap(); 2335 let wallet = wallet_for_address(&finalizers, &leader); 2336 let error = ledger.prepare_next_block(wallet.address(), 1).unwrap_err(); 2337 2338 assert!(format!("{error:#}").contains("block must include at least one burn transaction")); 2339 } 2340 2341 #[test] 2342 fn blinded_transaction_expiry_cannot_exceed_protocol_window() { 2343 let alice = Wallet::from_seed("blinded-window-finalizer-alice"); 2344 let bob = Wallet::from_seed("blinded-window-finalizer-bob"); 2345 let carol = Wallet::from_seed("blinded-window-carol"); 2346 let finalizers = [alice, bob]; 2347 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2348 let max_expiry = ledger 2349 .height() 2350 .saturating_add(MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS); 2351 2352 ledger.build_blinded_burn(&carol, 3, 7, max_expiry).unwrap(); 2353 2354 let error = ledger 2355 .build_blinded_burn(&carol, 3, 7, max_expiry + 1) 2356 .unwrap_err(); 2357 assert!(format!("{error:#}").contains("expiry is too far in the future")); 2358 2359 let mut forged = ledger 2360 .build_blinded_burn(&carol, 3, 7, max_expiry) 2361 .unwrap() 2362 .transaction; 2363 forged.expires_at_height = max_expiry + 1; 2364 forged.commitment = blinded_transaction_commitment(&forged).unwrap(); 2365 let error = ledger.submit_blinded_transaction(forged).unwrap_err(); 2366 assert!(format!("{error:#}").contains("expiry is too far in the future")); 2367 } 2368 2369 #[test] 2370 fn blinded_transaction_does_not_satisfy_plaintext_burn_requirement() { 2371 let alice = Wallet::from_seed("blinded-no-burn-finalizer-alice"); 2372 let bob = Wallet::from_seed("blinded-no-burn-finalizer-bob"); 2373 let carol = Wallet::from_seed("blinded-no-burn-carol"); 2374 let finalizers = [alice.clone(), bob.clone()]; 2375 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2376 let blinded = ledger 2377 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 2378 .unwrap(); 2379 ledger 2380 .submit_blinded_transaction(blinded.transaction) 2381 .unwrap(); 2382 2383 let leader = ledger.expected_leader_for_next_block().unwrap(); 2384 let wallet = wallet_for_address(&finalizers, &leader); 2385 let error = ledger.prepare_next_block(wallet.address(), 1).unwrap_err(); 2386 2387 assert!(format!("{error:#}").contains("block must include at least one burn transaction")); 2388 } 2389 2390 #[test] 2391 fn revealed_blinded_transaction_cannot_be_included_again() { 2392 let alice = Wallet::from_seed("blinded-duplicate-finalizer-alice"); 2393 let bob = Wallet::from_seed("blinded-duplicate-finalizer-bob"); 2394 let carol = Wallet::from_seed("blinded-duplicate-carol"); 2395 let finalizers = [alice.clone(), bob.clone()]; 2396 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2397 let blinded = ledger 2398 .build_blinded_burn(&carol, 3, 7, ledger.height() + 6) 2399 .unwrap(); 2400 ledger 2401 .submit_blinded_transaction(blinded.transaction.clone()) 2402 .unwrap(); 2403 queue_next_leader_burn(&mut ledger, &finalizers); 2404 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2405 ledger 2406 .submit_blinded_reveal(blinded.reveal.clone()) 2407 .unwrap(); 2408 queue_next_leader_burn(&mut ledger, &finalizers); 2409 mine_preverified_as_next_leader_with_reveal_bundles(&mut ledger, &finalizers, 2); 2410 2411 let leader = ledger.expected_leader_for_next_block().unwrap(); 2412 let wallet = wallet_for_address(&finalizers, &leader); 2413 let filler_burn = ledger.build_burn(wallet, 1, 0).unwrap(); 2414 ledger.submit_transaction(filler_burn).unwrap(); 2415 let mut prepared = ledger 2416 .prepare_next_block(wallet.address(), ledger.tip().timestamp_ms + 1) 2417 .unwrap(); 2418 prepared 2419 .blinded_transactions 2420 .push(blinded.transaction.clone()); 2421 let block = prepared.finish(wallet, "preverified-vdf".to_string()); 2422 2423 let error = ledger 2424 .apply_preverified_block_at(block, u64::MAX) 2425 .unwrap_err(); 2426 2427 assert!(format!("{error:#}").contains("blinded transaction is already on chain")); 2428 } 2429 2430 #[test] 2431 fn pending_blinded_reveals_expose_revealed_transaction_data() { 2432 let alice = Wallet::from_seed("pending-reveal-data-finalizer-alice"); 2433 let bob = Wallet::from_seed("pending-reveal-data-finalizer-bob"); 2434 let carol = Wallet::from_seed("pending-reveal-data-carol"); 2435 let finalizers = [alice.clone(), bob.clone()]; 2436 let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2437 let blinded = ledger 2438 .build_blinded_burn(&carol, 3, 7, ledger.height() + 6) 2439 .unwrap(); 2440 let commitment = blinded.transaction.commitment.clone(); 2441 ledger 2442 .submit_blinded_transaction(blinded.transaction) 2443 .unwrap(); 2444 queue_next_leader_burn(&mut ledger, &finalizers); 2445 mine_preverified_as_next_leader(&mut ledger, &finalizers, 1); 2446 2447 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 2448 2449 let revealed = ledger.pending_revealed_blinded_transactions(); 2450 assert_eq!(revealed.len(), 1); 2451 assert_eq!(revealed[0].commitment, commitment); 2452 assert_eq!(revealed[0].height, ledger.height() + 1); 2453 assert_eq!(revealed[0].transaction.amount(), 3); 2454 assert_eq!(revealed[0].transaction.fee(), 7); 2455 assert!(revealed[0].transaction.is_burn()); 2456 } 2457 2458 #[test] 2459 fn abandoned_fork_blinded_transactions_return_to_mempool() { 2460 let alice = Wallet::from_seed("blinded-reorg-finalizer-alice"); 2461 let bob = Wallet::from_seed("blinded-reorg-finalizer-bob"); 2462 let carol = Wallet::from_seed("blinded-reorg-carol"); 2463 let finalizers = [alice.clone(), bob.clone()]; 2464 let mut local = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); 2465 let mut remote = local.clone(); 2466 let blinded = local 2467 .build_blinded_burn(&carol, 3, 7, local.height() + 8) 2468 .unwrap(); 2469 local 2470 .submit_blinded_transaction(blinded.transaction.clone()) 2471 .unwrap(); 2472 queue_next_leader_burn(&mut local, &finalizers); 2473 mine_preverified_as_next_leader(&mut local, &finalizers, 1); 2474 2475 for timestamp_ms in [1, 2] { 2476 let leader = remote.expected_leader_for_next_block().unwrap(); 2477 let wallet = wallet_for_address(&finalizers, &leader); 2478 let burn = remote.build_burn(wallet, 1, 0).unwrap(); 2479 remote.submit_transaction(burn).unwrap(); 2480 mine_preverified_as_next_leader(&mut remote, &finalizers, timestamp_ms); 2481 } 2482 2483 assert!( 2484 local 2485 .extend_from_preverified_snapshot_at(remote.snapshot(), u64::MAX) 2486 .unwrap() 2487 ); 2488 assert!(local.has_blinded_transaction(&blinded.transaction.commitment)); 2489 assert_eq!( 2490 local.pending_blinded_transactions(), 2491 std::slice::from_ref(&blinded.transaction) 2492 ); 2493 } 2494 2495 #[test] 2496 fn block_selection_includes_mine_action_after_required_block_burn() { 2497 let alice = Wallet::from_seed("mine-fixed-reward-select-alice"); 2498 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2499 2500 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2501 ledger.submit_transaction(burn).unwrap(); 2502 let mine = ledger.build_mine(alice.address()).unwrap(); 2503 ledger.submit_transaction(mine.clone()).unwrap(); 2504 2505 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2506 2507 assert_eq!( 2508 block.transactions.iter().filter(|tx| tx.is_burn()).count(), 2509 1 2510 ); 2511 assert!( 2512 block 2513 .transactions 2514 .iter() 2515 .any(|tx| tx.signature() == mine.signature()) 2516 ); 2517 assert_eq!(block.reward, MINE_FINALIZER_FEE); 2518 } 2519 2520 #[test] 2521 fn block_selection_can_skip_mine_action_when_space_is_limited() { 2522 let alice = Wallet::from_seed("mine-space-limit-alice"); 2523 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2524 ledger.launch_profile.max_block_transactions = 2; 2525 2526 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2527 ledger.submit_transaction(burn).unwrap(); 2528 let first_mine = ledger.build_mine(alice.address()).unwrap(); 2529 ledger.submit_transaction(first_mine).unwrap(); 2530 let second_mine = ledger.build_mine(alice.address()).unwrap(); 2531 ledger.submit_transaction(second_mine).unwrap(); 2532 2533 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2534 2535 assert_eq!(block.transactions.len(), 2); 2536 assert!(block.transactions.iter().any(Transaction::is_burn)); 2537 assert_eq!(block.reward, MINE_FINALIZER_FEE); 2538 } 2539 2540 #[test] 2541 fn pending_mine_outputs_are_not_spendable_until_confirmed() { 2542 let alice = Wallet::from_seed("pending-mine-spend-alice"); 2543 let bob = Wallet::from_seed("pending-mine-spend-bob"); 2544 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2545 2546 let mine = ledger.build_mine(alice.address()).unwrap(); 2547 let mine_outpoint = OutPoint { 2548 txid: mine.signature().to_string(), 2549 index: 0, 2550 }; 2551 ledger.submit_transaction(mine.clone()).unwrap(); 2552 2553 assert!( 2554 !ledger 2555 .available_utxos_for_address(alice.address()) 2556 .unwrap() 2557 .iter() 2558 .any(|(outpoint, _)| outpoint == &mine_outpoint) 2559 ); 2560 let pending_error = ledger 2561 .build_transfer_with_inputs( 2562 &alice, 2563 bob.address(), 2564 TEST_BURN_AMOUNT, 2565 0, 2566 std::slice::from_ref(&mine_outpoint), 2567 ) 2568 .unwrap_err(); 2569 assert!(format!("{pending_error:#}").contains("not spendable")); 2570 2571 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2572 ledger.submit_transaction(burn).unwrap(); 2573 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2574 assert!( 2575 block 2576 .transactions 2577 .iter() 2578 .any(|tx| tx.signature() == mine.signature()) 2579 ); 2580 ledger.apply_locally_mined_block(block).unwrap(); 2581 2582 assert!( 2583 ledger 2584 .available_utxos_for_address(alice.address()) 2585 .unwrap() 2586 .iter() 2587 .any(|(outpoint, _)| outpoint == &mine_outpoint) 2588 ); 2589 ledger 2590 .build_transfer_with_inputs( 2591 &alice, 2592 bob.address(), 2593 TEST_BURN_AMOUNT, 2594 0, 2595 std::slice::from_ref(&mine_outpoint), 2596 ) 2597 .unwrap(); 2598 } 2599 2600 #[test] 2601 fn burns_built_after_pending_mine_do_not_spend_pending_mine_output() { 2602 let alice = Wallet::from_seed("pending-mine-burn-alice"); 2603 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2604 2605 let mine = ledger.build_mine(alice.address()).unwrap(); 2606 let mine_outpoint = OutPoint { 2607 txid: mine.signature().to_string(), 2608 index: 0, 2609 }; 2610 ledger.submit_transaction(mine).unwrap(); 2611 2612 let burn = ledger.build_burn(&alice, TEST_BURN_AMOUNT, 0).unwrap(); 2613 2614 let Transaction::Burn { inputs, .. } = &burn else { 2615 panic!("expected burn transaction"); 2616 }; 2617 assert!(!inputs.iter().any(|input| input.outpoint == mine_outpoint)); 2618 } 2619 2620 #[test] 2621 fn pending_blinded_transactions_with_spent_inputs_are_pruned_after_block_apply() { 2622 let alice = Wallet::from_seed("pending-blind-spent-prune-alice"); 2623 let mut mempool_ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2624 let mut block_ledger = mempool_ledger.clone(); 2625 let amount = mempool_ledger.balance_of(alice.address()); 2626 let blinded = mempool_ledger 2627 .build_blinded_burn(&alice, amount, 0, mempool_ledger.height() + 4) 2628 .unwrap(); 2629 mempool_ledger 2630 .submit_blinded_transaction(blinded.transaction.clone()) 2631 .unwrap(); 2632 2633 let burn = block_ledger.build_burn(&alice, amount, 0).unwrap(); 2634 let Transaction::Burn { inputs, .. } = &burn else { 2635 panic!("expected burn transaction"); 2636 }; 2637 assert!(blinded.transaction.inputs.iter().any(|input| { 2638 inputs 2639 .iter() 2640 .any(|burn_input| burn_input.outpoint == input.outpoint) 2641 })); 2642 block_ledger.submit_transaction(burn).unwrap(); 2643 let block = block_ledger.mine_next_block(&alice, 1).unwrap(); 2644 2645 mempool_ledger.apply_block(block).unwrap(); 2646 2647 assert!(mempool_ledger.pending_blinded_transactions().is_empty()); 2648 } 2649 2650 #[test] 2651 fn block_selection_limits_mine_actions_per_anchor() { 2652 let alice = Wallet::from_seed("mine-anchor-limit-selection-alice"); 2653 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2654 2655 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2656 ledger.submit_transaction(burn).unwrap(); 2657 let first_mine = ledger.build_mine(alice.address()).unwrap(); 2658 ledger.submit_transaction(first_mine.clone()).unwrap(); 2659 let second_mine = ledger.build_mine(alice.address()).unwrap(); 2660 ledger.submit_transaction(second_mine.clone()).unwrap(); 2661 assert_eq!(ledger.pending().len(), 3); 2662 let block = ledger.mine_next_block(&alice, 1).unwrap(); 2663 2664 assert_eq!(block.transactions.len(), 3); 2665 assert!(block.transactions.iter().any(Transaction::is_burn)); 2666 let included_mines = block 2667 .transactions 2668 .iter() 2669 .filter(|transaction| matches!(transaction, Transaction::Mine { .. })) 2670 .count(); 2671 assert_eq!(included_mines, MINE_ACTIONS_PER_ANCHOR_LIMIT); 2672 assert!( 2673 block 2674 .transactions 2675 .iter() 2676 .any(|tx| tx.signature() == first_mine.signature()) 2677 ); 2678 assert!( 2679 block 2680 .transactions 2681 .iter() 2682 .any(|tx| tx.signature() == second_mine.signature()) 2683 ); 2684 assert_ne!(first_mine.signature(), second_mine.signature()); 2685 assert_eq!(block.reward, first_mine.fee() + second_mine.fee()); 2686 } 2687 2688 #[test] 2689 fn blocks_reject_too_many_mine_actions_for_one_anchor() { 2690 let alice = Wallet::from_seed("mine-anchor-limit-block-alice"); 2691 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2692 2693 let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); 2694 let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); 2695 let third_mine = test_mine_with_salt(&ledger, alice.address(), 3); 2696 let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); 2697 ledger.submit_transaction(burn).unwrap(); 2698 let mut block = ledger 2699 .prepare_next_block( 2700 alice.address(), 2701 ledger 2702 .tip() 2703 .timestamp_ms 2704 .saturating_add(VDF_TARGET_BLOCK_MS), 2705 ) 2706 .unwrap() 2707 .finish(&alice, "preverified-vdf".to_string()); 2708 block.transactions.push(first_mine); 2709 block.transactions.push(second_mine); 2710 block.transactions.push(third_mine); 2711 block.reward = fee_reward(&block.transactions).unwrap(); 2712 block.hash = block.compute_hash(); 2713 2714 let error = ledger 2715 .apply_preverified_block_at(block, u64::MAX) 2716 .unwrap_err(); 2717 2718 assert!(format!("{error:#}").contains("mine actions per anchor limit")); 2719 } 2720 2721 #[test] 2722 fn mempool_rejects_mine_actions_above_anchor_limit() { 2723 let alice = Wallet::from_seed("mine-anchor-limit-mempool-alice"); 2724 let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); 2725 2726 let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); 2727 let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); 2728 let third_mine = test_mine_with_salt(&ledger, alice.address(), 3); 2729 ledger.submit_transaction(first_mine).unwrap(); 2730 ledger.submit_transaction(second_mine).unwrap(); 2731 let error = ledger.submit_transaction(third_mine).unwrap_err(); 2732 2733 assert!(format!("{error:#}").contains("mine transaction anchor limit reached")); 2734 } 2735 2736 #[test] 2737 fn mine_difficulty_increases_when_issuance_exceeds_target_window() { 2738 let alice = Wallet::from_seed("mine-difficulty-up-alice"); 2739 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2740 2741 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2742 apply_preverified_burn_block_with_mines(&mut ledger, &alice, 2); 2743 } 2744 2745 assert_eq!( 2746 ledger.current_mine_difficulty_bits(), 2747 MINE_DIFFICULTY_BITS + 1 2748 ); 2749 let mine = ledger.build_mine(alice.address()).unwrap(); 2750 let Transaction::Mine { 2751 difficulty_bits, .. 2752 } = mine 2753 else { 2754 panic!("expected mine action"); 2755 }; 2756 assert_eq!(difficulty_bits, MINE_DIFFICULTY_BITS + 1); 2757 } 2758 2759 #[test] 2760 fn mine_difficulty_decreases_when_issuance_is_below_target_window() { 2761 let alice = Wallet::from_seed("mine-difficulty-down-alice"); 2762 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2763 2764 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2765 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2766 } 2767 2768 assert_eq!( 2769 ledger.current_mine_difficulty_bits(), 2770 MINE_DIFFICULTY_BITS - MINE_MAX_RETARGET_STEP_BITS 2771 ); 2772 2773 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2774 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2775 } 2776 2777 assert_eq!( 2778 ledger.current_mine_difficulty_bits(), 2779 MINE_MIN_DIFFICULTY_BITS 2780 ); 2781 2782 for _ in 0..MINE_RETARGET_WINDOW_BLOCKS { 2783 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2784 } 2785 2786 assert_eq!( 2787 ledger.current_mine_difficulty_bits(), 2788 MINE_MIN_DIFFICULTY_BITS 2789 ); 2790 } 2791 2792 #[test] 2793 fn mine_actions_expire_when_anchor_is_too_old() { 2794 let alice = Wallet::from_seed("mine-anchor-expiry-alice"); 2795 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2796 let stale_mine = ledger.build_mine(alice.address()).unwrap(); 2797 2798 for _ in 0..=MINE_MAX_ANCHOR_AGE_BLOCKS { 2799 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2800 } 2801 2802 let error = ledger.submit_transaction(stale_mine).unwrap_err(); 2803 assert!(format!("{error:#}").contains("mine transaction anchor is too old")); 2804 } 2805 2806 #[test] 2807 fn pending_mine_actions_are_removed_when_anchor_expires() { 2808 let alice = Wallet::from_seed("pending-mine-anchor-expiry-alice"); 2809 let bob = Wallet::from_seed("pending-mine-anchor-expiry-bob"); 2810 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2811 ledger.launch_profile.max_block_transactions = 1; 2812 let stale_mine = ledger.build_mine(bob.address()).unwrap(); 2813 ledger.submit_transaction(stale_mine.clone()).unwrap(); 2814 2815 for _ in 0..=MINE_MAX_ANCHOR_AGE_BLOCKS { 2816 mine_burn_block_with_mines(&mut ledger, &alice, 0); 2817 } 2818 2819 assert!( 2820 ledger 2821 .pending() 2822 .iter() 2823 .all(|tx| tx.signature() != stale_mine.signature()) 2824 ); 2825 } 2826 2827 #[test] 2828 fn stratum_mine_header_proof_is_validated() { 2829 let alice = Wallet::from_seed("stratum-proof-alice"); 2830 let ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2831 let anchor = ledger.tip().hash.clone(); 2832 let difficulty_bits = ledger.current_mine_difficulty_bits(); 2833 let template = ledger 2834 .stratum_mine_template(alice.address(), anchor, 1, difficulty_bits) 2835 .unwrap(); 2836 2837 let mut accepted = None; 2838 for nonce in 0_u32..50_000 { 2839 let result = ledger.build_stratum_mine( 2840 template.clone(), 2841 StratumMineShare { 2842 extranonce2: [0, 0, 0, 0], 2843 header_nonce: nonce.to_le_bytes(), 2844 }, 2845 ); 2846 if let Ok(tx) = result { 2847 accepted = Some(tx); 2848 break; 2849 } 2850 } 2851 2852 let tx = accepted.expect("expected Stratum proof within search range"); 2853 let Transaction::Mine { 2854 proof_header, 2855 signature, 2856 .. 2857 } = tx 2858 else { 2859 panic!("expected mine action"); 2860 }; 2861 assert_eq!(proof_header.as_deref().unwrap_or_default().len(), 160); 2862 assert!(hash_meets_difficulty(&signature, difficulty_bits)); 2863 } 2864 2865 #[test] 2866 fn stratum_mine_salt_allows_multiple_actions_for_same_anchor() { 2867 let alice = Wallet::from_seed("stratum-salt-alice"); 2868 let mut ledger = ledger_with_allocation(&alice, 100 * MICRO_IUNA); 2869 let anchor = ledger.tip().hash.clone(); 2870 let difficulty_bits = ledger.current_mine_difficulty_bits(); 2871 2872 for salt in [1, 2] { 2873 let template = ledger 2874 .stratum_mine_template(alice.address(), anchor.clone(), salt, difficulty_bits) 2875 .unwrap(); 2876 let mut accepted = None; 2877 for nonce in 0_u32..50_000 { 2878 let result = ledger.build_stratum_mine( 2879 template.clone(), 2880 StratumMineShare { 2881 extranonce2: [0, 0, 0, 0], 2882 header_nonce: nonce.to_le_bytes(), 2883 }, 2884 ); 2885 if let Ok(tx) = result { 2886 accepted = Some(tx); 2887 break; 2888 } 2889 } 2890 let tx = accepted.expect("expected Stratum proof within search range"); 2891 assert!(ledger.submit_transaction(tx).unwrap()); 2892 } 2893 2894 assert_eq!(ledger.pending().len(), 2); 2895 let salts = ledger 2896 .pending() 2897 .iter() 2898 .map(|tx| match tx { 2899 Transaction::Mine { salt, .. } => *salt, 2900 _ => panic!("expected mine action"), 2901 }) 2902 .collect::<BTreeSet<_>>(); 2903 assert_eq!(salts, BTreeSet::from([1, 2])); 2904 }