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 }