ledger_queries.rs (14501B)
1 use std::collections::{BTreeMap, BTreeSet}; 2 3 use anyhow::{Context, Result, bail}; 4 5 use super::blinded::{ 6 ActiveBlindedTransaction, blinded_envelope_fee_for_transaction, decrypt_blinded_transaction, 7 }; 8 use super::genesis::balances_from_utxos; 9 use super::mine_policy::mine_anchor; 10 use super::ticket::{ 11 BurnTicket, apply_finalizer_ticket_effects, genesis_tickets, ranked_tickets_for_height, 12 tickets_created_by_block, tickets_created_by_transactions, 13 }; 14 use super::{ 15 Amount, BlindedReveal, BlindedTransaction, Block, BurnLeaderRank, ChainSnapshot, ChainStatus, 16 LaunchProfile, Ledger, OutPoint, RevealCommitteeMember, RevealedBlindedTransaction, 17 Transaction, TxOutput, reveal_committee_slot_count, 18 }; 19 20 fn apply_historical_ticket_block( 21 block: &Block, 22 launch_profile: &LaunchProfile, 23 tickets: &mut Vec<BurnTicket>, 24 active_blinded: &mut BTreeMap<String, ActiveBlindedTransaction>, 25 ) -> Result<()> { 26 apply_finalizer_ticket_effects(block, tickets)?; 27 tickets.extend(tickets_created_by_block(block, launch_profile)?); 28 let mut revealed_transactions = Vec::new(); 29 for reveal in block.all_blinded_reveals() { 30 let active = active_blinded.get(&reveal.commitment).with_context(|| { 31 format!( 32 "block {} reveals unknown blinded transaction {}", 33 block.height, reveal.commitment 34 ) 35 })?; 36 let transaction = decrypt_blinded_transaction(&active.transaction, reveal)?; 37 if matches!(transaction, Transaction::Mine { .. }) { 38 bail!("mine actions are public and cannot be blinded"); 39 } 40 if blinded_envelope_fee_for_transaction(&transaction) != active.transaction.fee { 41 bail!( 42 "block {} blinded reveal fee does not match envelope", 43 block.height 44 ); 45 } 46 revealed_transactions.push(transaction); 47 active_blinded.remove(&reveal.commitment); 48 } 49 tickets.extend(tickets_created_by_transactions( 50 block.height, 51 &revealed_transactions, 52 launch_profile, 53 )?); 54 active_blinded.retain(|_, active| block.height < active.transaction.expires_at_height); 55 for transaction in &block.blinded_transactions { 56 active_blinded.insert( 57 transaction.commitment.clone(), 58 ActiveBlindedTransaction { 59 transaction: transaction.clone(), 60 locked_outputs: Vec::new(), 61 included_height: block.height, 62 included_by: block.miner.clone(), 63 }, 64 ); 65 } 66 Ok(()) 67 } 68 69 impl Ledger { 70 pub fn snapshot(&self) -> ChainSnapshot { 71 ChainSnapshot { 72 genesis_allocations: self.genesis_allocations.clone(), 73 vdf_rounds: self.initial_vdf_rounds, 74 launch_profile: self.launch_profile.clone(), 75 blocks: self.chain.clone(), 76 } 77 } 78 79 pub fn status(&self) -> ChainStatus { 80 self.status_with_balances(true) 81 } 82 83 pub fn light_status(&self) -> ChainStatus { 84 self.status_with_balances(false) 85 } 86 87 fn status_with_balances(&self, include_balances: bool) -> ChainStatus { 88 ChainStatus { 89 height: self.tip().height, 90 tip_hash: self.tip().hash.clone(), 91 next_leader: self.expected_leader_for_next_block(), 92 launch_profile_hash: self.launch_profile.hash(), 93 mine_reward: self.mine_reward, 94 current_mine_difficulty_bits: self.current_mine_difficulty_bits(), 95 balances: include_balances 96 .then(|| balances_from_utxos(&self.utxos)) 97 .unwrap_or_default(), 98 pending_transactions: self.pending.len() 99 + self.pending_blinded.len() 100 + self.pending_reveals.len(), 101 } 102 } 103 104 pub fn tip_hash(&self) -> &str { 105 &self.tip().hash 106 } 107 108 pub fn chain(&self) -> &[Block] { 109 &self.chain 110 } 111 112 pub fn burn_leader_ranks_for_block(&self, height: u64) -> Result<Vec<BurnLeaderRank>> { 113 Ok(self 114 .burn_leader_ranks_for_blocks([height])? 115 .remove(&height) 116 .unwrap_or_default()) 117 } 118 119 pub fn burn_leader_ranks_for_blocks<I>( 120 &self, 121 heights: I, 122 ) -> Result<BTreeMap<u64, Vec<BurnLeaderRank>>> 123 where 124 I: IntoIterator<Item = u64>, 125 { 126 let mut requested = heights.into_iter().collect::<BTreeSet<_>>(); 127 let mut ranks_by_height = BTreeMap::new(); 128 if requested.remove(&0) { 129 ranks_by_height.insert(0, Vec::new()); 130 } 131 if requested.is_empty() { 132 return Ok(ranks_by_height); 133 } 134 135 let mut tickets = genesis_tickets( 136 &self.genesis_allocations, 137 &self.chain[0], 138 &self.launch_profile, 139 )?; 140 let mut active_blinded = BTreeMap::<String, ActiveBlindedTransaction>::new(); 141 let mut next_block_index = 1; 142 143 for height in requested { 144 let parent_index = height.checked_sub(1).context("block height underflows")? as usize; 145 let parent = self 146 .chain 147 .get(parent_index) 148 .with_context(|| format!("missing parent block for height {height}"))?; 149 while let Some(block) = self.chain.get(next_block_index) { 150 if block.height >= height { 151 break; 152 } 153 apply_historical_ticket_block( 154 block, 155 &self.launch_profile, 156 &mut tickets, 157 &mut active_blinded, 158 )?; 159 next_block_index += 1; 160 } 161 162 ranks_by_height.insert( 163 height, 164 ranked_tickets_for_height(parent, height, &tickets) 165 .into_iter() 166 .enumerate() 167 .map(|(rank, ticket)| BurnLeaderRank { 168 rank: rank as u32, 169 ticket_id: ticket.id, 170 owner: ticket.owner, 171 amount: ticket.amount, 172 eligible_from_height: ticket.eligible_from_height, 173 eligible_until_height: ticket.eligible_until_height, 174 }) 175 .collect(), 176 ); 177 } 178 179 Ok(ranks_by_height) 180 } 181 182 pub fn reveal_committee_for_next_block(&self) -> Vec<RevealCommitteeMember> { 183 self.reveal_committee_for_height(self.tip().height + 1) 184 } 185 186 pub fn reveal_committee_for_height(&self, height: u64) -> Vec<RevealCommitteeMember> { 187 let ranked = ranked_tickets_for_height(self.tip(), height, &self.tickets); 188 let mut selected = Vec::new(); 189 if !ranked.is_empty() { 190 selected.push(0); 191 } 192 for index in (0..ranked.len()).rev() { 193 if selected.len() >= reveal_committee_slot_count(ranked.len()) { 194 break; 195 } 196 if !selected.contains(&index) { 197 selected.push(index); 198 } 199 } 200 selected 201 .into_iter() 202 .enumerate() 203 .filter_map(|(slot, rank)| { 204 let ticket = ranked.get(rank)?.clone(); 205 Some(RevealCommitteeMember { 206 slot: u8::try_from(slot).ok()?, 207 rank: u32::try_from(rank).ok()?, 208 ticket_id: ticket.id, 209 owner: ticket.owner, 210 amount: ticket.amount, 211 }) 212 }) 213 .collect() 214 } 215 216 pub fn genesis_hash(&self) -> &str { 217 &self.chain[0].hash 218 } 219 220 pub fn is_setup_placeholder(&self) -> bool { 221 self.height() == 0 222 && self.genesis_allocations.is_empty() 223 && self.chain[0].transactions.is_empty() 224 && self.pending.is_empty() 225 } 226 227 pub fn height(&self) -> u64 { 228 self.tip().height 229 } 230 231 pub fn recent_blocks(&self, limit: usize) -> Vec<Block> { 232 self.chain.iter().rev().take(limit).cloned().collect() 233 } 234 235 pub fn blocks_before(&self, before_height: u64, limit: usize) -> Vec<Block> { 236 self.chain 237 .iter() 238 .rev() 239 .filter(|block| block.height < before_height) 240 .take(limit) 241 .cloned() 242 .collect() 243 } 244 245 pub fn blocks_from(&self, from_height: u64, limit: usize) -> Vec<Block> { 246 if limit == 0 { 247 return Vec::new(); 248 } 249 self.chain 250 .iter() 251 .filter(|block| block.height >= from_height) 252 .take(limit) 253 .cloned() 254 .collect() 255 } 256 257 pub fn block_by_hash(&self, hash: &str) -> Option<Block> { 258 self.chain.iter().find(|block| block.hash == hash).cloned() 259 } 260 261 pub fn has_block(&self, hash: &str) -> bool { 262 self.chain.iter().any(|block| block.hash == hash) 263 } 264 265 pub fn pending(&self) -> &[Transaction] { 266 &self.pending 267 } 268 269 pub fn pending_blinded_transactions(&self) -> &[BlindedTransaction] { 270 &self.pending_blinded 271 } 272 273 pub fn pending_blinded_reveals(&self) -> &[BlindedReveal] { 274 &self.pending_reveals 275 } 276 277 pub fn pending_revealed_blinded_transactions(&self) -> Vec<RevealedBlindedTransaction> { 278 self.pending_reveals 279 .iter() 280 .filter_map(|reveal| { 281 let active = self.active_blinded.get(&reveal.commitment)?; 282 let transaction = self.pending_reveal_transaction(reveal).ok()?; 283 Some(RevealedBlindedTransaction { 284 height: self.height().saturating_add(1), 285 commitment: reveal.commitment.clone(), 286 included_by: active.included_by.clone(), 287 transaction, 288 }) 289 }) 290 .collect() 291 } 292 293 pub(crate) fn drop_pending_blinded_conflicting_with_transaction( 294 &mut self, 295 transaction: &Transaction, 296 ) { 297 let spent = transaction 298 .inputs() 299 .iter() 300 .map(|input| input.outpoint.clone()) 301 .collect::<BTreeSet<_>>(); 302 self.pending_blinded.retain(|blinded| { 303 !blinded 304 .inputs 305 .iter() 306 .any(|input| spent.contains(&input.outpoint)) 307 }); 308 } 309 310 pub(crate) fn clear_pending_blinded_transactions(&mut self) { 311 self.pending_blinded.clear(); 312 } 313 314 pub(crate) fn clear_pending_transactions(&mut self) { 315 self.pending.clear(); 316 } 317 318 pub fn orphan_transactions(&self) -> &[Transaction] { 319 &self.orphans 320 } 321 322 pub fn transaction_by_signature(&self, signature: &str) -> Option<Transaction> { 323 self.pending 324 .iter() 325 .chain(self.orphans.iter()) 326 .chain( 327 self.chain 328 .iter() 329 .flat_map(|block| block.transactions.iter()), 330 ) 331 .find(|tx| tx.signature() == signature) 332 .cloned() 333 } 334 335 pub fn has_transaction(&self, signature: &str) -> bool { 336 self.transaction_by_signature(signature).is_some() 337 } 338 339 pub fn pending_mine_count_for_anchor(&self, anchor: &str) -> usize { 340 self.pending 341 .iter() 342 .filter(|tx| mine_anchor(tx) == Some(anchor)) 343 .count() 344 } 345 346 pub fn has_blinded_transaction(&self, commitment: &str) -> bool { 347 self.pending_blinded 348 .iter() 349 .any(|transaction| transaction.commitment == commitment) 350 || self.active_blinded.contains_key(commitment) 351 || self.chain.iter().any(|block| { 352 block 353 .blinded_transactions 354 .iter() 355 .any(|tx| tx.commitment == commitment) 356 }) 357 } 358 359 pub fn has_unrevealed_blinded_transaction(&self, commitment: &str) -> bool { 360 self.pending_blinded 361 .iter() 362 .any(|transaction| transaction.commitment == commitment) 363 || self.active_blinded.contains_key(commitment) 364 } 365 366 pub fn has_active_blinded_transaction(&self, commitment: &str) -> bool { 367 self.active_blinded.contains_key(commitment) 368 } 369 370 pub fn has_blinded_reveal(&self, commitment: &str) -> bool { 371 self.pending_reveals 372 .iter() 373 .any(|reveal| reveal.commitment == commitment) 374 || self.chain.iter().any(|block| { 375 block 376 .all_blinded_reveals() 377 .iter() 378 .any(|reveal| reveal.commitment == commitment) 379 }) 380 } 381 382 pub fn vdf_rounds(&self) -> u64 { 383 self.vdf_rounds 384 } 385 386 pub fn launch_profile(&self) -> &LaunchProfile { 387 &self.launch_profile 388 } 389 390 pub fn current_mine_difficulty_bits(&self) -> u32 { 391 self.mine_difficulty_bits_for_anchor_height(self.tip().height) 392 } 393 394 pub fn mine_difficulty_bits_at_height(&self, height: u64) -> u32 { 395 self.mine_difficulty_bits_for_anchor_height(height.min(self.tip().height)) 396 } 397 398 pub fn balance_of(&self, address: &str) -> Amount { 399 self.utxos 400 .values() 401 .filter(|output| output.address == address) 402 .map(|output| output.amount) 403 .sum() 404 } 405 406 pub fn utxos_for_address(&self, address: &str) -> Vec<(OutPoint, TxOutput)> { 407 self.utxos 408 .iter() 409 .filter(|(_, output)| output.address == address) 410 .map(|(outpoint, output)| (outpoint.clone(), output.clone())) 411 .collect() 412 } 413 414 pub fn all_utxos(&self) -> Vec<(OutPoint, TxOutput)> { 415 self.utxos 416 .iter() 417 .map(|(outpoint, output)| (outpoint.clone(), output.clone())) 418 .collect() 419 } 420 421 pub fn available_utxos_for_address(&self, address: &str) -> Result<Vec<(OutPoint, TxOutput)>> { 422 Ok(self 423 .utxos_after_spendable_pending()? 424 .into_iter() 425 .filter(|(_, output)| output.address == address) 426 .collect()) 427 } 428 429 pub fn next_nonce(&self, address: &str) -> u64 { 430 self.utxos 431 .keys() 432 .chain( 433 self.pending 434 .iter() 435 .flat_map(|tx| tx.inputs().iter().map(|input| &input.outpoint)), 436 ) 437 .filter(|outpoint| outpoint.txid.contains(address)) 438 .count() as u64 439 + 1 440 } 441 }