iuna

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

app.rs (24113B)


      1 use std::{
      2     collections::{BTreeMap, BTreeSet},
      3     sync::{
      4         Arc,
      5         atomic::{AtomicBool, Ordering},
      6     },
      7     time::{SystemTime, UNIX_EPOCH},
      8 };
      9 
     10 use anyhow::Result;
     11 use tokio::sync::Mutex;
     12 
     13 use crate::domain::{
     14     Amount, BlindedReveal, BlindedTransaction, BuiltBlindedTransaction, Ledger,
     15     MINE_ACTIONS_PER_ANCHOR_LIMIT, PreparedBlock, RevealBundle, Transaction, run_vdf,
     16 };
     17 
     18 mod automatic_mining;
     19 mod gossip;
     20 mod helpers;
     21 mod in_memory_network;
     22 mod ledger_view;
     23 mod node_lifecycle;
     24 mod owned_blinded;
     25 mod peer_book;
     26 mod receive;
     27 mod status;
     28 mod types;
     29 mod wallet;
     30 pub use in_memory_network::InMemoryNetwork;
     31 pub use peer_book::{PeerBook, PeerDirection, PeerInfo};
     32 pub use types::{
     33     AutoMineOutcome, AutoMinePlan, BlockInventory, ExternalMineJob, FeeEstimate, GossipEnvelope,
     34     LaunchProfileStatus, MiningStatus, NodeConfig, NodeStatus, ProtocolHello, StratumStatus,
     35 };
     36 use wallet::NodeWallet;
     37 
     38 pub type SharedNode = Arc<Mutex<NodeCore>>;
     39 pub type SharedPeerBook = Arc<Mutex<PeerBook>>;
     40 
     41 pub const DEFAULT_BURN_PER_BLOCK: Amount = 0;
     42 pub const DEFAULT_VDF_ROUNDS: u32 = 67_000_000;
     43 pub const PROTOCOL_VERSION: u32 = 1;
     44 pub const NETWORK_ID: &str = "iuna-devnet-v3";
     45 pub const BLOCK_REQUEST_LIMIT: usize = 128;
     46 pub const TRANSACTION_BATCH_LIMIT: usize = 128;
     47 const IMPORT_REBROADCAST_LIMIT: usize = 128;
     48 pub const PEER_MISBEHAVIOR_BAN_SCORE: u32 = 3;
     49 pub const PEER_MISBEHAVIOR_BAN_MS: u64 = 10 * 60 * 1_000;
     50 pub const PEER_CLOCK_OFFSET_ACCEPTANCE_MS: i64 = 10 * 60 * 1_000;
     51 const PEER_CLOCK_OFFSET_STALE_MS: u64 = 20 * 60 * 1_000;
     52 const AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK: u64 = 100_000;
     53 const AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS: u64 = 60_000;
     54 const REVEAL_BUNDLE_COLLECTION_MS: u64 = 30_000;
     55 const AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = 1;
     56 const AUTO_BLOCK_ANCHOR_BURN_FEE: Amount = 0;
     57 static DEBUG_LOGGING: AtomicBool = AtomicBool::new(false);
     58 
     59 pub fn set_debug_logging(enabled: bool) {
     60     DEBUG_LOGGING.store(enabled, Ordering::Relaxed);
     61 }
     62 
     63 pub fn debug_logging_enabled() -> bool {
     64     DEBUG_LOGGING.load(Ordering::Relaxed)
     65 }
     66 
     67 #[derive(Clone, Debug, Eq, PartialEq)]
     68 struct AutoPowMineCursor {
     69     anchor: String,
     70     salt: u64,
     71     next_nonce: u64,
     72     searched: u64,
     73 }
     74 
     75 #[derive(Clone, Debug)]
     76 pub struct AutoPowMineJob {
     77     ledger: Ledger,
     78     recipient: String,
     79     anchor: String,
     80     salt: u64,
     81     start_nonce: u64,
     82     max_attempts: u64,
     83 }
     84 
     85 impl AutoPowMineJob {
     86     pub fn anchor(&self) -> &str {
     87         &self.anchor
     88     }
     89 
     90     pub fn search(self) -> Result<(Self, crate::domain::MineSearchOutcome)> {
     91         let outcome = self.ledger.search_mine(
     92             self.recipient.clone(),
     93             self.salt,
     94             self.start_nonce,
     95             self.max_attempts,
     96         )?;
     97         Ok((self, outcome))
     98     }
     99 }
    100 
    101 #[derive(Clone, Debug)]
    102 pub struct NodeCore {
    103     wallet: NodeWallet,
    104     ledger: Ledger,
    105     automatic_mining_enabled: bool,
    106     pow_mining_enabled: bool,
    107     pow_mining_workers: u8,
    108     burn_per_block: Amount,
    109     burn_fee: Amount,
    110     recovery_vdf_top_rank_percent: u8,
    111     last_auto_burn_height: Option<u64>,
    112     last_auto_anchor_burn_height: Option<u64>,
    113     last_auto_pow_mine_anchor: Option<String>,
    114     last_auto_pow_mine_status: Option<String>,
    115     auto_pow_mine_cursor: Option<AutoPowMineCursor>,
    116     owned_blinded_transactions: BTreeMap<String, BlindedTransaction>,
    117     owned_blinded_reveals: BTreeMap<String, BlindedReveal>,
    118     owned_blinded_payloads: BTreeMap<String, Transaction>,
    119     owned_blinded_outbox_version: u64,
    120     reveal_bundles: BTreeMap<(u64, u8), RevealBundle>,
    121     equivocated_reveal_bundle_slots: BTreeSet<(u64, u8)>,
    122     reveal_bundle_collection_started: Option<(u64, u64)>,
    123     local_block_anchor_burn: Option<(u64, Transaction)>,
    124     outbox: Vec<GossipEnvelope>,
    125 }
    126 
    127 pub fn now_ms() -> u64 {
    128     SystemTime::now()
    129         .duration_since(UNIX_EPOCH)
    130         .expect("system time is before unix epoch")
    131         .as_millis() as u64
    132 }
    133 
    134 #[cfg(test)]
    135 mod tests {
    136     use std::collections::BTreeMap;
    137 
    138     use crate::domain::{
    139         GenesisBurn, Ledger, MICRO_IUNA, OutPoint, Transaction, VDF_TARGET_BLOCK_MS, Wallet,
    140         run_vdf,
    141     };
    142 
    143     use super::{
    144         InMemoryNetwork, NodeCore, REVEAL_BUNDLE_COLLECTION_MS,
    145         helpers::transaction_input_outpoints,
    146     };
    147 
    148     fn wallet_for_address<'a>(wallets: &'a [Wallet], address: &str) -> &'a Wallet {
    149         wallets
    150             .iter()
    151             .find(|wallet| wallet.address() == address)
    152             .unwrap_or_else(|| panic!("missing wallet for address {address}"))
    153     }
    154 
    155     fn queue_auto_pow_mine_action(node: &mut NodeCore) -> Transaction {
    156         node.set_pow_mining_enabled(true);
    157         (0..10_000)
    158             .find_map(|timestamp| node.prepare_automatic_mining(timestamp).pow_mined)
    159             .expect("test node should find a PoW mine action")
    160     }
    161 
    162     fn assert_block_has_mine_action(block: &crate::domain::Block) {
    163         assert!(
    164             block
    165                 .transactions
    166                 .iter()
    167                 .any(|transaction| matches!(transaction, Transaction::Mine { .. })),
    168             "block {} should include a mine action",
    169             block.height
    170         );
    171     }
    172 
    173     #[test]
    174     fn automatic_finalization_includes_reveals_with_two_nodes_and_one_burner() {
    175         let finalizer = Wallet::from_seed("single-burner-reveal-finalizer");
    176         let wallet = Wallet::from_seed("single-burner-reveal-wallet");
    177         let mut allocations = BTreeMap::new();
    178         allocations.insert(finalizer.address().to_string(), 10 * MICRO_IUNA);
    179         allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA);
    180         let ledger = Ledger::new_with_genesis_burns(
    181             allocations,
    182             vec![GenesisBurn::new(finalizer.address(), MICRO_IUNA)],
    183             1,
    184         )
    185         .unwrap();
    186         let mut network = InMemoryNetwork::default();
    187         network.insert(
    188             "finalizer",
    189             NodeCore::from_ledger_with_burn_fee_and_enabled(
    190                 finalizer.clone(),
    191                 ledger.clone(),
    192                 true,
    193                 MICRO_IUNA / 10,
    194                 1,
    195             ),
    196         );
    197         network.insert("wallet", NodeCore::from_ledger(wallet.clone(), ledger, 0));
    198 
    199         let blinded = network
    200             .node_mut("wallet")
    201             .unwrap()
    202             .blinded_burn_with_fee(MICRO_IUNA / 10, 1, 4)
    203             .unwrap();
    204         network.deliver_until_idle().unwrap();
    205 
    206         let commit_plan = network
    207             .node_mut("finalizer")
    208             .unwrap()
    209             .prepare_automatic_finalization(1);
    210         let commit_work = commit_plan
    211             .work
    212             .expect("finalizer should prepare commit block");
    213         let commit_vdf = run_vdf(commit_work.vdf_seed(), commit_work.vdf_rounds());
    214         let commit_block = network
    215             .node_mut("finalizer")
    216             .unwrap()
    217             .complete_prepared_block_at(commit_work, commit_vdf, 1)
    218             .unwrap();
    219         let wallet_commitment = blinded.commitment.clone();
    220         assert!(
    221             commit_block
    222                 .blinded_transactions
    223                 .iter()
    224                 .any(|transaction| transaction.commitment == wallet_commitment),
    225             "first block should commit the wallet's blinded burn"
    226         );
    227         network.deliver_until_idle().unwrap();
    228         assert!(
    229             network
    230                 .node("finalizer")
    231                 .unwrap()
    232                 .ledger()
    233                 .pending_blinded_reveals()
    234                 .iter()
    235                 .any(|reveal| reveal.commitment == wallet_commitment),
    236             "finalizer should have received the reveal before building the next block"
    237         );
    238         assert!(
    239             network
    240                 .node("wallet")
    241                 .unwrap()
    242                 .ledger()
    243                 .pending_blinded_reveals()
    244                 .iter()
    245                 .any(|reveal| reveal.commitment == wallet_commitment),
    246             "wallet node should also keep the reveal in its mempool"
    247         );
    248 
    249         let reveal_plan = network
    250             .node_mut("finalizer")
    251             .unwrap()
    252             .prepare_automatic_finalization(2);
    253         assert!(reveal_plan.work.is_none());
    254         assert!(
    255             reveal_plan
    256                 .skipped_reason
    257                 .as_deref()
    258                 .unwrap_or_default()
    259                 .contains("collecting blinded reveals")
    260         );
    261         let reveal_plan = network
    262             .node_mut("finalizer")
    263             .unwrap()
    264             .prepare_automatic_finalization(REVEAL_BUNDLE_COLLECTION_MS + 2);
    265         let reveal_work = reveal_plan
    266             .work
    267             .expect("finalizer should prepare reveal block");
    268         let reveal_vdf = run_vdf(reveal_work.vdf_seed(), reveal_work.vdf_rounds());
    269         let reveal_block = network
    270             .node_mut("finalizer")
    271             .unwrap()
    272             .complete_prepared_block_at(reveal_work, reveal_vdf, 2)
    273             .unwrap();
    274 
    275         assert!(
    276             reveal_block
    277                 .all_blinded_reveals()
    278                 .iter()
    279                 .any(|reveal| reveal.commitment == wallet_commitment),
    280             "automatic finalization should include the pending reveal without requiring an extra mempool poll"
    281         );
    282     }
    283 
    284     #[test]
    285     fn genesis_transfer_arriving_during_vdf_with_peer_mines_does_not_stall_following_blocks() {
    286         let miner = Wallet::from_seed("during-vdf-miner");
    287         let (_finalizer, finalizer_node, block2_work) = (0..1_000)
    288             .find_map(|seed_index| {
    289                 let finalizer = Wallet::from_seed(&format!("during-vdf-finalizer-{seed_index}"));
    290                 let mut allocations = BTreeMap::new();
    291                 allocations.insert(finalizer.address().to_string(), 10 * MICRO_IUNA);
    292                 let mut ledger = Ledger::new_with_genesis_burns(
    293                     allocations,
    294                     vec![GenesisBurn::new(finalizer.address(), MICRO_IUNA)],
    295                     1,
    296                 )
    297                 .unwrap();
    298                 let split = ledger
    299                     .build_transfer(&finalizer, finalizer.address(), MICRO_IUNA / 10, 0)
    300                     .ok()?;
    301                 let split_change = OutPoint {
    302                     txid: split.signature().to_string(),
    303                     index: 1,
    304                 };
    305                 ledger.submit_transaction(split).ok()?;
    306                 let burn = ledger
    307                     .build_burn_with_inputs(&finalizer, 1, 0, &[split_change])
    308                     .ok()?;
    309                 ledger.submit_transaction(burn).ok()?;
    310                 let block1 = ledger.mine_next_block(&finalizer, 1).ok()?;
    311                 assert!(block1.blinded_transactions.is_empty());
    312                 assert!(
    313                     !block1
    314                         .transactions
    315                         .iter()
    316                         .any(|transaction| matches!(transaction, Transaction::Mine { .. })),
    317                     "node B joins after block 1, so block 1 should not include B's mine action"
    318                 );
    319                 ledger.apply_block(block1).ok()?;
    320                 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(
    321                     finalizer.clone(),
    322                     ledger,
    323                     true,
    324                     0,
    325                     100,
    326                 );
    327                 let block2_plan = node.prepare_automatic_finalization(2);
    328                 let block2_work = block2_plan.work?;
    329                 let (_, anchor_burn) = node.local_block_anchor_burn.clone()?;
    330                 let anchor_inputs = transaction_input_outpoints(&anchor_burn);
    331                 let anchor_total = node
    332                     .ledger()
    333                     .utxos_for_address(finalizer.address())
    334                     .iter()
    335                     .filter(|(outpoint, _)| anchor_inputs.contains(outpoint))
    336                     .map(|(_, output)| output.amount)
    337                     .sum::<u64>();
    338                 if anchor_total >= 200_000 {
    339                     return None;
    340                 }
    341                 Some((finalizer, node, block2_work))
    342             })
    343             .expect("test should find a seed where block 2 anchor uses the small reward UTXO");
    344         let mut network = InMemoryNetwork::default();
    345         network.insert("finalizer", finalizer_node);
    346 
    347         let miner_ledger =
    348             Ledger::from_snapshot(network.node("finalizer").unwrap().chain_snapshot()).unwrap();
    349         network.insert(
    350             "miner",
    351             NodeCore::from_ledger(miner.clone(), miner_ledger, 0),
    352         );
    353 
    354         queue_auto_pow_mine_action(network.node_mut("miner").unwrap());
    355         network.deliver_until_idle().unwrap();
    356         network
    357             .node_mut("finalizer")
    358             .unwrap()
    359             .transfer_with_fee_rate(miner.address(), MICRO_IUNA / 10, 100, &[])
    360             .unwrap();
    361         let blinded = network
    362             .node("finalizer")
    363             .unwrap()
    364             .ledger()
    365             .pending_blinded_transactions()
    366             .last()
    367             .cloned()
    368             .expect("A should queue the A -> B blinded transfer while block 2 VDF is running");
    369         network.deliver_until_idle().unwrap();
    370         assert!(
    371             network
    372                 .node("finalizer")
    373                 .unwrap()
    374                 .ledger()
    375                 .pending_blinded_transactions()
    376                 .iter()
    377                 .any(|tx| tx.commitment == blinded.commitment),
    378             "the finalizer should receive the blinded tx while block 2 VDF is running"
    379         );
    380 
    381         let block2_vdf = run_vdf(block2_work.vdf_seed(), block2_work.vdf_rounds());
    382         let block2 = network
    383             .node_mut("finalizer")
    384             .unwrap()
    385             .complete_prepared_block_at(block2_work, block2_vdf, 2)
    386             .unwrap();
    387         assert!(
    388             block2.blinded_transactions.is_empty(),
    389             "block 2 work was prepared before the blinded tx arrived"
    390         );
    391         assert!(
    392             !block2
    393                 .transactions
    394                 .iter()
    395                 .any(|transaction| matches!(transaction, Transaction::Mine { .. })),
    396             "block 2 work was prepared before B's mine action arrived"
    397         );
    398         network.deliver_until_idle().unwrap();
    399 
    400         let block3_outcome = network
    401             .node_mut("finalizer")
    402             .unwrap()
    403             .automatic_mine_once(3);
    404         assert!(
    405             block3_outcome.block.is_some(),
    406             "finalizer should keep producing after the during-VDF blinded tx: {:?}",
    407             block3_outcome.skipped_reason
    408         );
    409         let block3 = block3_outcome.block.unwrap();
    410         assert!(
    411             block3
    412                 .transactions
    413                 .first()
    414                 .is_some_and(Transaction::is_burn),
    415             "the mandatory anchor burn must be selected before during-VDF mempool items"
    416         );
    417         let committed_blinded = block3
    418             .blinded_transactions
    419             .iter()
    420             .any(|tx| tx.commitment == blinded.commitment);
    421         assert_block_has_mine_action(&block3);
    422         network.deliver_until_idle().unwrap();
    423 
    424         queue_auto_pow_mine_action(network.node_mut("miner").unwrap());
    425         network.deliver_until_idle().unwrap();
    426         let block4_started_at = block3.timestamp_ms.saturating_add(1);
    427         let mut block4_outcome = network
    428             .node_mut("finalizer")
    429             .unwrap()
    430             .automatic_mine_once(block4_started_at);
    431         if block4_outcome
    432             .skipped_reason
    433             .as_deref()
    434             .is_some_and(|reason| reason.contains("collecting blinded reveals"))
    435         {
    436             block4_outcome = network.node_mut("finalizer").unwrap().automatic_mine_once(
    437                 block4_started_at.saturating_add(REVEAL_BUNDLE_COLLECTION_MS + 1),
    438             );
    439         }
    440         let block4 = block4_outcome
    441             .block
    442             .expect("finalizer should keep producing the next block");
    443         if committed_blinded {
    444             assert!(
    445                 block4
    446                     .all_blinded_reveals()
    447                     .iter()
    448                     .any(|reveal| reveal.commitment == blinded.commitment),
    449                 "the committed during-VDF blinded tx should reveal in a later block"
    450             );
    451         } else {
    452             assert!(
    453                 network
    454                     .node("finalizer")
    455                     .unwrap()
    456                     .ledger()
    457                     .pending_blinded_transactions()
    458                     .is_empty(),
    459                 "a conflicting during-VDF blinded tx should be pruned after the anchor burn spends its input"
    460             );
    461             assert!(
    462                 network
    463                     .node("finalizer")
    464                     .unwrap()
    465                     .owned_blinded_transactions()
    466                     .is_empty(),
    467                 "owned blinded state should not keep rebroadcasting a pruned tx"
    468             );
    469             assert!(
    470                 block4.all_blinded_reveals().is_empty(),
    471                 "a pruned blinded tx was never committed, so there should be no reveal"
    472             );
    473         }
    474         assert_block_has_mine_action(&block4);
    475     }
    476 
    477     #[test]
    478     fn own_blinded_transaction_arriving_during_vdf_does_not_starve_next_anchor_burn() {
    479         let recipient = Wallet::from_seed("during-vdf-own-recipient");
    480         let (mut node, block2_work, transfer_outpoint, transfer_amount) = (0..1_000)
    481             .find_map(|seed_index| {
    482                 let finalizer =
    483                     Wallet::from_seed(&format!("during-vdf-own-finalizer-{seed_index}"));
    484                 let mut allocations = BTreeMap::new();
    485                 allocations.insert(finalizer.address().to_string(), 10 * MICRO_IUNA);
    486                 let ledger = Ledger::new_with_genesis_burns(
    487                     allocations,
    488                     vec![GenesisBurn::new(finalizer.address(), MICRO_IUNA)],
    489                     1,
    490                 )
    491                 .unwrap();
    492                 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(
    493                     finalizer.clone(),
    494                     ledger,
    495                     true,
    496                     0,
    497                     1_000,
    498                 );
    499                 node.set_pow_mining_enabled(true);
    500                 let block1 = node.automatic_mine_once(1).block?;
    501                 assert!(block1.blinded_transactions.is_empty());
    502 
    503                 let block2_plan = node.prepare_automatic_finalization(2);
    504                 let block2_work = block2_plan.work?;
    505                 let (_, anchor_burn) = node.local_block_anchor_burn.clone()?;
    506                 let anchor_inputs = transaction_input_outpoints(&anchor_burn);
    507                 let utxos = node.ledger().utxos_for_address(finalizer.address());
    508                 let anchor_total = utxos
    509                     .iter()
    510                     .filter(|(outpoint, _)| anchor_inputs.contains(outpoint))
    511                     .map(|(_, output)| output.amount)
    512                     .sum::<u64>();
    513                 let (transfer_outpoint, transfer_output) =
    514                     utxos.into_iter().find(|(outpoint, output)| {
    515                         !anchor_inputs.contains(outpoint) && output.amount > MICRO_IUNA
    516                     })?;
    517                 if anchor_total >= 2_000_000 {
    518                     return None;
    519                 }
    520                 Some((
    521                     node,
    522                     block2_work,
    523                     transfer_outpoint,
    524                     transfer_output.amount.min(MICRO_IUNA) / 10,
    525                 ))
    526             })
    527             .expect("test should find a seed with live-like small-anchor/large-change UTXOs");
    528 
    529         node.transfer_with_fee_spending(
    530             recipient.address(),
    531             transfer_amount,
    532             1,
    533             &[transfer_outpoint],
    534         )
    535         .expect("wallet tx created while block 2 VDF is running");
    536         let blinded = node
    537             .ledger()
    538             .pending_blinded_transactions()
    539             .last()
    540             .cloned()
    541             .expect("wallet tx should be queued as a blinded transaction");
    542 
    543         let block2_vdf = run_vdf(block2_work.vdf_seed(), block2_work.vdf_rounds());
    544         let block2 = node
    545             .complete_prepared_block_at(block2_work, block2_vdf, 2)
    546             .unwrap();
    547         assert!(
    548             block2.blinded_transactions.is_empty(),
    549             "block 2 work was prepared before the blinded tx arrived"
    550         );
    551         assert!(
    552             node.ledger()
    553                 .pending_blinded_transactions()
    554                 .iter()
    555                 .any(|tx| tx.commitment == blinded.commitment),
    556             "the during-VDF blinded tx should remain pending for block 3"
    557         );
    558 
    559         let block3_outcome = node.automatic_mine_once(3);
    560         assert!(
    561             block3_outcome.block.is_some(),
    562             "pending own blinded tx must not starve the next anchor burn: {:?}",
    563             block3_outcome.skipped_reason
    564         );
    565         let block3 = block3_outcome.block.unwrap();
    566         assert!(
    567             block3.blinded_transactions.is_empty()
    568                 || block3
    569                     .blinded_transactions
    570                     .iter()
    571                     .any(|tx| tx.commitment == blinded.commitment),
    572             "block 3 may include the during-VDF tx, but must not stall when the anchor burn has priority"
    573         );
    574     }
    575 
    576     #[test]
    577     fn locally_produced_blocks_import_on_independent_peer_ledger() {
    578         let alice = Wallet::from_seed("producer-parity-alice");
    579         let bob = Wallet::from_seed("producer-parity-bob");
    580         let carol = Wallet::from_seed("producer-parity-carol");
    581         let wallets = [alice.clone(), bob.clone(), carol.clone()];
    582         let mut allocations = BTreeMap::new();
    583         for wallet in &wallets {
    584             allocations.insert(wallet.address().to_string(), 20 * MICRO_IUNA);
    585         }
    586         let genesis_burns = wallets
    587             .iter()
    588             .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    589             .collect();
    590         let mut producer_ledger =
    591             Ledger::new_with_genesis_burns(allocations, genesis_burns, 1).unwrap();
    592         let mut peer_ledger = producer_ledger.clone();
    593 
    594         for step in 0..8 {
    595             assert_eq!(
    596                 producer_ledger.status().tip_hash,
    597                 peer_ledger.status().tip_hash
    598             );
    599             let leader = producer_ledger
    600                 .expected_leader_for_next_block()
    601                 .expect("test chain should have an eligible leader");
    602             let leader_wallet = wallet_for_address(&wallets, &leader).clone();
    603             let timestamp_ms = (step + 1) as u64 * VDF_TARGET_BLOCK_MS;
    604             let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(
    605                 leader_wallet.clone(),
    606                 producer_ledger.clone(),
    607                 true,
    608                 MICRO_IUNA / 10,
    609                 1,
    610             );
    611             let plan = node.prepare_automatic_finalization(timestamp_ms);
    612             assert!(plan.burned.is_some());
    613 
    614             match step % 3 {
    615                 0 => {
    616                     let _ = node.blinded_burn_with_fee(1, 0, node.chain_height() + 4);
    617                 }
    618                 1 => {
    619                     let recipient = wallets[(step + 1) % wallets.len()].address();
    620                     let _ =
    621                         node.blinded_transfer_with_fee(recipient, 1, 0, node.chain_height() + 4);
    622                 }
    623                 _ => {}
    624             }
    625 
    626             let work = node
    627                 .prepare_next_block_with_local_anchor(timestamp_ms)
    628                 .unwrap();
    629             let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds());
    630             let block = node
    631                 .complete_prepared_block_at(work, vdf_output, timestamp_ms)
    632                 .unwrap();
    633             peer_ledger.apply_block_at(block, u64::MAX).unwrap();
    634             producer_ledger = node.clone_ledger();
    635             assert_eq!(
    636                 producer_ledger.status().tip_hash,
    637                 peer_ledger.status().tip_hash
    638             );
    639         }
    640     }
    641 }