ledger_apply.rs (15152B)
1 use std::collections::BTreeSet; 2 3 use anyhow::{Context, Result, bail}; 4 5 use super::blinded::{ 6 ActiveBlindedTransaction, credit_blinded_fee_outputs, credit_expired_blinded_outputs, 7 }; 8 use super::ledger_ops::{ 9 apply_transaction, block_reward, credit_reward_output, ensure_block_has_burn, 10 ensure_valid_recovery_block, spend_blinded_inputs, validate_block_blinded_items, 11 verify_leader_proof, 12 }; 13 use super::mine_policy::ensure_mine_anchor_limit; 14 use super::ticket::{ 15 apply_finalizer_ticket_effects, ticket_block_min_timestamp, tickets_created_by_block, 16 tickets_created_by_transactions, 17 }; 18 use super::transaction::{blinded_transaction_inputs_available, transaction_inputs_available}; 19 use super::{ 20 Amount, BLOCK_MEDIAN_TIME_PAST_WINDOW, Block, FinalizerMode, Ledger, 21 MAX_BLOCK_TIMESTAMP_FUTURE_DRIFT_MS, RevealBundleSection, Transaction, 22 blinded_reveal_finalizer_fee, unix_now_ms, verify_vdf, 23 }; 24 25 impl Ledger { 26 pub fn apply_block(&mut self, block: Block) -> Result<()> { 27 self.apply_block_at(block, unix_now_ms()) 28 } 29 30 pub(crate) fn apply_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { 31 self.apply_block_with_vdf_policy(block, true, now_ms) 32 } 33 34 pub(crate) fn block_requires_vdf_verification_at( 35 &self, 36 block: &Block, 37 now_ms: u64, 38 ) -> Result<bool> { 39 self.precheck_block_without_vdf_at(block, now_ms) 40 } 41 42 pub fn apply_locally_mined_block(&mut self, block: Block) -> Result<()> { 43 self.apply_self_produced_block_at(block, unix_now_ms()) 44 } 45 46 pub(crate) fn apply_self_produced_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { 47 self.verify_self_produced_block_at(&block, now_ms)?; 48 self.apply_preverified_block_at(block, now_ms) 49 } 50 51 pub(crate) fn verify_self_produced_block_at(&self, block: &Block, now_ms: u64) -> Result<()> { 52 let mut verifier = self.clone(); 53 verifier.apply_preverified_block_at(block.clone(), now_ms)?; 54 Ok(()) 55 } 56 57 pub(crate) fn apply_preverified_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { 58 self.apply_block_with_vdf_policy(block, false, now_ms) 59 } 60 61 fn apply_block_with_vdf_policy( 62 &mut self, 63 block: Block, 64 should_verify_vdf: bool, 65 now_ms: u64, 66 ) -> Result<()> { 67 if !self.precheck_block_without_vdf_at(&block, now_ms)? { 68 return Ok(()); 69 } 70 71 if should_verify_vdf && !verify_vdf(&block.vdf_seed(), block.vdf_rounds, &block.vdf_output) 72 { 73 bail!("block VDF output is invalid"); 74 } 75 76 let reveal_bundle_slot_count = self.reveal_committee_for_height(block.height).len(); 77 let mut utxos = self.utxos.clone(); 78 let mut signatures = BTreeSet::new(); 79 let mut revealed_transactions = Vec::new(); 80 let mut aggregated_reveal_finalizer_fees = 0_u64; 81 for tx in &block.transactions { 82 if !signatures.insert(tx.signature()) { 83 bail!("duplicate transaction in block"); 84 } 85 self.validate_transaction_terms(tx)?; 86 apply_transaction(tx, &mut utxos)?; 87 } 88 let mut revealed_commitments = BTreeSet::new(); 89 for reveal in block.all_blinded_reveals() { 90 if !revealed_commitments.insert(reveal.commitment.clone()) { 91 bail!("duplicate blinded reveal in block"); 92 } 93 let active = self 94 .active_blinded 95 .get(&reveal.commitment) 96 .context("blinded reveal does not reference an active blinded transaction")? 97 .clone(); 98 let tx = self.decrypt_active_blinded(&active, reveal)?; 99 self.apply_revealed_blinded_transaction(&active, &tx, &mut utxos)?; 100 credit_blinded_fee_outputs( 101 &mut utxos, 102 &active, 103 &block.miner, 104 &tx, 105 &block.reveal_bundle_section.signatures, 106 reveal_bundle_slot_count, 107 true, 108 )?; 109 aggregated_reveal_finalizer_fees = aggregated_reveal_finalizer_fees 110 .checked_add(blinded_reveal_finalizer_fee( 111 tx.fee(), 112 block.included_reveal_bundle_count(), 113 reveal_bundle_slot_count, 114 )) 115 .context("aggregated reveal finalizer fees overflow")?; 116 revealed_transactions.push(tx); 117 } 118 for (commitment, active) in &self.active_blinded { 119 if !revealed_commitments.contains(commitment) 120 && block.height >= active.transaction.expires_at_height 121 { 122 credit_expired_blinded_outputs(&mut utxos, active)?; 123 } 124 } 125 let expected_reward = block_reward(&block.transactions, aggregated_reveal_finalizer_fees)?; 126 if block.reward != expected_reward { 127 bail!("block reward is invalid"); 128 } 129 let mined_signatures = block 130 .transactions 131 .iter() 132 .map(|tx| tx.signature().to_string()) 133 .collect::<BTreeSet<_>>(); 134 let included_blinded = block 135 .blinded_transactions 136 .iter() 137 .map(|transaction| transaction.commitment.clone()) 138 .collect::<BTreeSet<_>>(); 139 let revealed_blinded = block 140 .all_blinded_reveals() 141 .into_iter() 142 .map(|reveal| reveal.commitment.clone()) 143 .collect::<BTreeSet<_>>(); 144 let mut new_active_blinded = Vec::new(); 145 for transaction in &block.blinded_transactions { 146 let locked_outputs = spend_blinded_inputs(transaction, &mut utxos)?; 147 new_active_blinded.push(( 148 transaction.commitment.clone(), 149 ActiveBlindedTransaction { 150 transaction: transaction.clone(), 151 locked_outputs, 152 included_height: block.height, 153 included_by: block.miner.clone(), 154 }, 155 )); 156 } 157 let mut tickets = self.tickets.clone(); 158 apply_finalizer_ticket_effects(&block, &mut tickets)?; 159 tickets.extend(tickets_created_by_block(&block, &self.launch_profile)?); 160 tickets.extend(tickets_created_by_transactions( 161 block.height, 162 &revealed_transactions, 163 &self.launch_profile, 164 )?); 165 credit_reward_output(&mut utxos, &block)?; 166 self.utxos = utxos; 167 self.tickets = tickets; 168 self.chain.push(block); 169 let new_height = self.height(); 170 self.active_blinded.retain(|commitment, active| { 171 !revealed_blinded.contains(commitment) 172 && new_height < active.transaction.expires_at_height 173 }); 174 for (commitment, active) in new_active_blinded { 175 self.active_blinded.insert(commitment, active); 176 } 177 let available = self.utxos.clone(); 178 let pending = std::mem::take(&mut self.pending); 179 self.pending = pending 180 .into_iter() 181 .filter(|tx| { 182 !mined_signatures.contains(tx.signature()) 183 && transaction_inputs_available(tx, &available) 184 && self.validate_transaction_terms(tx).is_ok() 185 }) 186 .collect(); 187 let orphans = std::mem::take(&mut self.orphans); 188 self.orphans = orphans 189 .into_iter() 190 .filter(|tx| { 191 !mined_signatures.contains(tx.signature()) 192 && self.validate_transaction_terms(tx).is_ok() 193 }) 194 .collect(); 195 let pending_blinded = std::mem::take(&mut self.pending_blinded); 196 self.pending_blinded = pending_blinded 197 .into_iter() 198 .filter(|transaction| { 199 !included_blinded.contains(&transaction.commitment) 200 && new_height < transaction.expires_at_height 201 && blinded_transaction_inputs_available(transaction, &available) 202 && self.validate_blinded_transaction(transaction).is_ok() 203 }) 204 .collect(); 205 let pending_reveals = std::mem::take(&mut self.pending_reveals); 206 self.pending_reveals = pending_reveals 207 .into_iter() 208 .filter(|reveal| { 209 !revealed_blinded.contains(&reveal.commitment) 210 && self.pending_reveal_transaction(reveal).is_ok() 211 }) 212 .collect(); 213 self.promote_orphan_transactions()?; 214 self.vdf_rounds = self.next_vdf_rounds_after_tip(); 215 Ok(()) 216 } 217 218 fn precheck_block_without_vdf_at(&self, block: &Block, now_ms: u64) -> Result<bool> { 219 if block.height <= self.tip().height { 220 let existing = self 221 .chain 222 .get(block.height as usize) 223 .with_context(|| format!("local chain has no block at height {}", block.height))?; 224 if existing.hash == block.hash { 225 return Ok(false); 226 } 227 bail!( 228 "block at height {} conflicts with local chain", 229 block.height 230 ); 231 } 232 233 let expected_height = self.tip().height + 1; 234 if block.height != expected_height { 235 bail!( 236 "expected block height {expected_height}, got {}", 237 block.height 238 ); 239 } 240 if block.prev_hash != self.tip().hash { 241 bail!("block does not extend local tip"); 242 } 243 if block.compute_hash() != block.hash { 244 bail!("block hash is invalid"); 245 } 246 let reveal_bundle_slot_count = self.reveal_committee_for_height(block.height).len(); 247 if block.reward != self.expected_reward_for_block(block, reveal_bundle_slot_count)? { 248 bail!("block reward is invalid"); 249 } 250 let expected_vdf_rounds = self.expected_vdf_rounds_for_block(block)?; 251 if block.vdf_rounds != expected_vdf_rounds { 252 bail!("block VDF rounds are invalid"); 253 } 254 if block.timestamp_ms <= self.tip().timestamp_ms { 255 bail!("block timestamp must increase"); 256 } 257 if block.finalizer_mode == FinalizerMode::Ticket { 258 let min_timestamp = ticket_block_min_timestamp(self.tip(), block.finalizer_rank)?; 259 if block.timestamp_ms < min_timestamp { 260 bail!( 261 "block timestamp is before finalizer rank {} time slot {min_timestamp}", 262 block.finalizer_rank 263 ); 264 } 265 } 266 let median_time_past = self.median_time_past(); 267 if block.timestamp_ms <= median_time_past { 268 bail!("block timestamp must exceed median time past"); 269 } 270 let max_future_timestamp = now_ms.saturating_add(MAX_BLOCK_TIMESTAMP_FUTURE_DRIFT_MS); 271 if block.timestamp_ms > max_future_timestamp { 272 bail!("block timestamp is too far in the future"); 273 } 274 if block.transactions.len() > self.launch_profile.max_block_transactions { 275 bail!("block has too many transactions"); 276 } 277 let block_item_count = block.transactions.len() 278 + block.blinded_transactions.len() 279 + block.all_blinded_reveals().len(); 280 if block_item_count > self.launch_profile.max_block_transactions { 281 bail!("block has too many transaction items"); 282 } 283 if block.serialized_size_bytes()? > self.launch_profile.max_block_bytes { 284 bail!("block exceeds max block size"); 285 } 286 ensure_mine_anchor_limit(block.height, &block.transactions)?; 287 ensure_block_has_burn(&block.transactions)?; 288 self.validate_reveal_bundle_section_for_block( 289 block.height, 290 &block.prev_hash, 291 &block.reveal_bundle_section, 292 )?; 293 validate_block_blinded_items(block, self)?; 294 match block.finalizer_mode { 295 FinalizerMode::Ticket => { 296 let selected_ticket = self 297 .ticket_for_finalizer_rank(block.height, block.finalizer_rank) 298 .context("no selected ticket for block finalizer rank")?; 299 if selected_ticket.owner != block.miner { 300 bail!( 301 "block finalizer {} is not selected for rank {}", 302 block.miner, 303 block.finalizer_rank 304 ); 305 } 306 if block 307 .leader_proof 308 .as_ref() 309 .is_none_or(|proof| proof.ticket_id != selected_ticket.id) 310 { 311 bail!("block does not prove the selected leader ticket"); 312 } 313 verify_leader_proof(block, &self.tickets)?; 314 } 315 FinalizerMode::Recovery => { 316 ensure_valid_recovery_block(block, self.tip())?; 317 } 318 } 319 320 Ok(true) 321 } 322 323 fn median_time_past(&self) -> u64 { 324 let mut timestamps = self 325 .chain 326 .iter() 327 .rev() 328 .take(BLOCK_MEDIAN_TIME_PAST_WINDOW) 329 .map(|block| block.timestamp_ms) 330 .collect::<Vec<_>>(); 331 timestamps.sort_unstable(); 332 timestamps[timestamps.len() / 2] 333 } 334 335 pub(super) fn expected_reward_for_next_block( 336 &self, 337 transactions: &[Transaction], 338 reveal_bundle_section: &RevealBundleSection, 339 ) -> Result<Amount> { 340 let height = self.tip().height + 1; 341 let reveal_bundle_slot_count = self.reveal_committee_for_height(height).len(); 342 let aggregate = 343 self.aggregate_reveal_finalizer_fees(reveal_bundle_section, reveal_bundle_slot_count)?; 344 block_reward(transactions, aggregate) 345 } 346 347 fn expected_reward_for_block( 348 &self, 349 block: &Block, 350 reveal_bundle_slot_count: usize, 351 ) -> Result<Amount> { 352 let aggregate = self.aggregate_reveal_finalizer_fees( 353 &block.reveal_bundle_section, 354 reveal_bundle_slot_count, 355 )?; 356 block_reward(&block.transactions, aggregate) 357 } 358 359 fn aggregate_reveal_finalizer_fees( 360 &self, 361 reveal_bundle_section: &RevealBundleSection, 362 reveal_bundle_slot_count: usize, 363 ) -> Result<Amount> { 364 reveal_bundle_section 365 .all_reveals() 366 .into_iter() 367 .try_fold(0_u64, |total, reveal| { 368 let active = self 369 .active_blinded 370 .get(&reveal.commitment) 371 .context("blinded reveal does not reference an active blinded transaction")?; 372 total 373 .checked_add(blinded_reveal_finalizer_fee( 374 active.transaction.fee, 375 reveal_bundle_section.included_bundle_count(), 376 reveal_bundle_slot_count, 377 )) 378 .context("aggregated reveal finalizer fees overflow") 379 }) 380 } 381 }