iuna

iuna

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

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 }