iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

ledger_consensus.rs (7547B)


      1 use anyhow::Result;
      2 
      3 use super::mine_policy::{MINE_RETARGET_WINDOW_BLOCKS, retarget_mine_difficulty_bits};
      4 use super::ticket::{
      5     BurnTicket, base_vdf_rounds_for_finalizer_rank, mine_action_count, ranked_tickets_for_height,
      6     vdf_rounds_for_finalizer_rank,
      7 };
      8 use super::vdf::{recent_vdf_retarget_average_observed_block_ms, retarget_vdf_rounds};
      9 use super::{Block, FinalizerMode, Ledger};
     10 
     11 impl Ledger {
     12     pub(super) fn next_vdf_rounds_after_tip(&self) -> u64 {
     13         let Some(tip) = self.chain.last() else {
     14             return self.vdf_rounds;
     15         };
     16         if tip.height < 2 {
     17             return self.vdf_rounds;
     18         }
     19 
     20         let Some(average_observed_ms) = recent_vdf_retarget_average_observed_block_ms(&self.chain)
     21         else {
     22             return self.vdf_rounds;
     23         };
     24         let base_rounds = base_vdf_rounds_for_finalizer_rank(tip.vdf_rounds, tip.finalizer_rank);
     25         retarget_vdf_rounds(base_rounds, average_observed_ms)
     26     }
     27 
     28     pub fn expected_leader_for_next_block(&self) -> Option<String> {
     29         self.selected_ticket_for_height(self.tip().height + 1)
     30             .map(|ticket| ticket.owner)
     31     }
     32 
     33     pub fn finalizer_rank_for_next_block(&self, miner: &str) -> Option<u32> {
     34         self.finalizer_ticket_for_miner(self.tip().height + 1, miner)
     35             .map(|(rank, _)| rank)
     36     }
     37 
     38     pub fn finalizer_rank_count_for_next_block(&self) -> usize {
     39         ranked_tickets_for_height(self.tip(), self.tip().height + 1, &self.tickets).len()
     40     }
     41 
     42     pub(super) fn selected_ticket_for_height(&self, height: u64) -> Option<BurnTicket> {
     43         self.ticket_for_finalizer_rank(height, 0)
     44     }
     45 
     46     pub(super) fn ticket_for_finalizer_rank(&self, height: u64, rank: u32) -> Option<BurnTicket> {
     47         ranked_tickets_for_height(self.tip(), height, &self.tickets)
     48             .get(rank as usize)
     49             .cloned()
     50     }
     51 
     52     pub(super) fn finalizer_ticket_for_miner(
     53         &self,
     54         height: u64,
     55         miner: &str,
     56     ) -> Option<(u32, BurnTicket)> {
     57         ranked_tickets_for_height(self.tip(), height, &self.tickets)
     58             .into_iter()
     59             .enumerate()
     60             .find(|(_, ticket)| ticket.owner == miner)
     61             .and_then(|(rank, ticket)| {
     62                 let rank = u32::try_from(rank).ok()?;
     63                 Some((rank, ticket))
     64             })
     65     }
     66 
     67     pub(super) fn vdf_rounds_for_finalizer_rank(&self, rank: u32) -> Result<u64> {
     68         vdf_rounds_for_finalizer_rank(self.vdf_rounds, rank)
     69     }
     70 
     71     pub(super) fn recovery_vdf_rounds(&self) -> Result<u64> {
     72         vdf_rounds_for_finalizer_rank(self.vdf_rounds, 0)
     73     }
     74 
     75     pub(super) fn expected_vdf_rounds_for_block(&self, block: &Block) -> Result<u64> {
     76         match block.finalizer_mode {
     77             FinalizerMode::Ticket => self.vdf_rounds_for_finalizer_rank(block.finalizer_rank),
     78             FinalizerMode::Recovery => self.recovery_vdf_rounds(),
     79         }
     80     }
     81 
     82     pub(super) fn mine_difficulty_bits_for_anchor_height(&self, anchor_height: u64) -> u32 {
     83         let completed_windows = anchor_height / MINE_RETARGET_WINDOW_BLOCKS;
     84         if let Ok(index) = usize::try_from(completed_windows)
     85             && let Some(difficulty) = self.mine_difficulty_windows.get(index)
     86         {
     87             return *difficulty;
     88         }
     89 
     90         // Tests and migration helpers may construct synthetic chains directly.
     91         // Keep a linear fallback for those callers; production ledgers update
     92         // the cache as each block is applied.
     93         mine_difficulty_windows_for_chain(
     94             &self.chain,
     95             self.launch_profile.mine_difficulty_bits,
     96             anchor_height,
     97         )
     98         .last()
     99         .copied()
    100         .unwrap_or(self.launch_profile.mine_difficulty_bits)
    101     }
    102 
    103     pub(super) fn update_mine_difficulty_cache_after_tip(&mut self) {
    104         let height = self.tip().height;
    105         if height == 0 || !height.is_multiple_of(MINE_RETARGET_WINDOW_BLOCKS) {
    106             return;
    107         }
    108         let expected_len = usize::try_from(height / MINE_RETARGET_WINDOW_BLOCKS)
    109             .unwrap_or(usize::MAX)
    110             .saturating_add(1);
    111         if self.mine_difficulty_windows.len() >= expected_len {
    112             return;
    113         }
    114         let mine_actions = self
    115             .chain
    116             .iter()
    117             .rev()
    118             .take(MINE_RETARGET_WINDOW_BLOCKS as usize)
    119             .map(mine_action_count)
    120             .sum();
    121         let previous = self
    122             .mine_difficulty_windows
    123             .last()
    124             .copied()
    125             .unwrap_or(self.launch_profile.mine_difficulty_bits);
    126         self.mine_difficulty_windows
    127             .push(retarget_mine_difficulty_bits(previous, mine_actions));
    128     }
    129 
    130     pub(super) fn tip(&self) -> &Block {
    131         self.chain
    132             .last()
    133             .expect("ledger is always initialized with genesis")
    134     }
    135 }
    136 
    137 fn mine_difficulty_windows_for_chain(
    138     chain: &[Block],
    139     initial_difficulty: u32,
    140     anchor_height: u64,
    141 ) -> Vec<u32> {
    142     let completed_windows = anchor_height / MINE_RETARGET_WINDOW_BLOCKS;
    143     let mut difficulties = Vec::with_capacity(
    144         usize::try_from(completed_windows)
    145             .unwrap_or_default()
    146             .saturating_add(1),
    147     );
    148     difficulties.push(initial_difficulty);
    149     let mut difficulty = initial_difficulty;
    150     for window in 1..=completed_windows {
    151         let window_end = window.saturating_mul(MINE_RETARGET_WINDOW_BLOCKS);
    152         let window_start = window_end + 1 - MINE_RETARGET_WINDOW_BLOCKS;
    153         let mine_actions = chain
    154             .iter()
    155             .filter(|block| window_start <= block.height && block.height <= window_end)
    156             .map(mine_action_count)
    157             .sum();
    158         difficulty = retarget_mine_difficulty_bits(difficulty, mine_actions);
    159         difficulties.push(difficulty);
    160     }
    161     difficulties
    162 }
    163 
    164 #[cfg(test)]
    165 mod tests {
    166     use std::collections::BTreeMap;
    167 
    168     use super::*;
    169     use crate::domain::{BurnBundleSection, Transaction};
    170 
    171     fn mine(anchor: &str, nonce: u64) -> Transaction {
    172         Transaction::Mine {
    173             recipient: "1".repeat(64),
    174             anchor: anchor.to_string(),
    175             salt: 1,
    176             nonce,
    177             difficulty_bits: 12,
    178             proof_header: None,
    179             signature: format!("{nonce:064x}"),
    180         }
    181     }
    182 
    183     #[test]
    184     fn applied_window_cache_preserves_retarget_results_for_constant_time_lookup() {
    185         let mut ledger = Ledger::new(BTreeMap::new(), 1);
    186         for height in 1..=20 {
    187             let parent = ledger.tip().clone();
    188             let mut block = parent.clone();
    189             block.height = height;
    190             block.prev_hash = parent.hash.clone();
    191             block.hash = format!("{height:064x}");
    192             block.burn_bundle_section = BurnBundleSection::default();
    193             block.transactions = if height <= 10 {
    194                 vec![mine(&parent.hash, height)]
    195             } else {
    196                 Vec::new()
    197             };
    198             ledger.chain.push(block);
    199             ledger.update_mine_difficulty_cache_after_tip();
    200         }
    201 
    202         assert_eq!(ledger.mine_difficulty_windows, vec![12, 12, 10]);
    203         assert_eq!(ledger.mine_difficulty_bits_for_anchor_height(9), 12);
    204         assert_eq!(ledger.mine_difficulty_bits_for_anchor_height(10), 12);
    205         assert_eq!(ledger.mine_difficulty_bits_for_anchor_height(20), 10);
    206 
    207         let mut uncached = ledger.clone();
    208         uncached.mine_difficulty_windows.truncate(1);
    209         assert_eq!(uncached.mine_difficulty_bits_for_anchor_height(20), 10);
    210     }
    211 }