iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }