iuna

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

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 }