iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

tests.rs (96687B)


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