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