iuna

iuna

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

ui_index.rs (11716B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use crate::domain::{
      4     Amount, BLINDED_COMMITTER_FEE_BPS, BLINDED_FEE_BPS_DENOMINATOR,
      5     BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BlindedTransaction, Block, BurnLeaderRank, ChainSnapshot,
      6     Ledger, MINE_REWARD, OutPoint, REVEAL_COMMITTEE_SIZE, RevealedBlindedTransaction, Transaction,
      7     TxOutput, blinded_reveal_finalizer_fee, hex_hash, reveal_committee_slot_count_for_height,
      8     revealed_blinded_transactions,
      9 };
     10 
     11 #[derive(Clone, Debug, Default, Eq, PartialEq)]
     12 pub(crate) struct UiChainIndex {
     13     pub(crate) tip_hash: Option<String>,
     14     pub(crate) outputs: BTreeMap<OutPoint, TxOutput>,
     15     pub(crate) revealed_by_height: BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
     16     pub(crate) burn_leader_ranks_by_hash: BTreeMap<String, Vec<BurnLeaderRank>>,
     17 }
     18 
     19 pub(crate) fn build_ui_chain_index(snapshot: &ChainSnapshot) -> UiChainIndex {
     20     UiChainIndex {
     21         tip_hash: snapshot.blocks.last().map(|block| block.hash.clone()),
     22         outputs: known_chain_output_index(snapshot),
     23         revealed_by_height: revealed_transactions_by_height(snapshot),
     24         burn_leader_ranks_by_hash: burn_leader_ranks_for_blocks(snapshot, &snapshot.blocks),
     25     }
     26 }
     27 
     28 pub(crate) fn revealed_transactions_by_height(
     29     snapshot: &ChainSnapshot,
     30 ) -> BTreeMap<u64, Vec<RevealedBlindedTransaction>> {
     31     revealed_blinded_transactions(snapshot)
     32         .unwrap_or_default()
     33         .into_iter()
     34         .fold(
     35             BTreeMap::<u64, Vec<RevealedBlindedTransaction>>::new(),
     36             |mut by_height, revealed| {
     37                 by_height.entry(revealed.height).or_default().push(revealed);
     38                 by_height
     39             },
     40         )
     41 }
     42 
     43 pub(crate) fn burn_leader_ranks_for_blocks(
     44     snapshot: &ChainSnapshot,
     45     blocks: &[Block],
     46 ) -> BTreeMap<String, Vec<BurnLeaderRank>> {
     47     let Some(ranks_by_height) = Ledger::from_persisted_snapshot(snapshot.clone())
     48         .ok()
     49         .and_then(|ledger| {
     50             ledger
     51                 .burn_leader_ranks_for_blocks(blocks.iter().map(|block| block.height))
     52                 .ok()
     53         })
     54     else {
     55         return BTreeMap::new();
     56     };
     57 
     58     blocks
     59         .iter()
     60         .filter_map(|block| {
     61             ranks_by_height
     62                 .get(&block.height)
     63                 .cloned()
     64                 .map(|ranks| (block.hash.clone(), ranks))
     65         })
     66         .collect()
     67 }
     68 
     69 fn known_chain_output_index(snapshot: &ChainSnapshot) -> BTreeMap<OutPoint, TxOutput> {
     70     let mut outputs = BTreeMap::new();
     71     for (address, amount) in &snapshot.genesis_allocations {
     72         if *amount == 0 {
     73             continue;
     74         }
     75         outputs.insert(
     76             genesis_allocation_outpoint(address),
     77             TxOutput {
     78                 address: address.clone(),
     79                 amount: *amount,
     80             },
     81         );
     82     }
     83     let revealed = revealed_blinded_transactions(snapshot).unwrap_or_default();
     84     let blocks_by_height = snapshot
     85         .blocks
     86         .iter()
     87         .map(|block| (block.height, block))
     88         .collect::<BTreeMap<_, _>>();
     89     let reveal_bundle_slots_by_height = reveal_bundle_slots_by_height(snapshot);
     90     let blinded_by_commitment = snapshot
     91         .blocks
     92         .iter()
     93         .flat_map(|block| block.blinded_transactions.iter())
     94         .map(|transaction| (transaction.commitment.clone(), transaction.clone()))
     95         .collect::<BTreeMap<_, _>>();
     96     for block in &snapshot.blocks {
     97         for transaction in &block.transactions {
     98             index_transaction_outputs(&mut outputs, transaction);
     99         }
    100         if block.reward > 0 {
    101             outputs.insert(
    102                 reward_outpoint(&block.hash),
    103                 TxOutput {
    104                     address: block.miner.clone(),
    105                     amount: block.reward,
    106                 },
    107             );
    108         }
    109     }
    110     for revealed in revealed {
    111         index_transaction_outputs(&mut outputs, &revealed.transaction);
    112         let fee = revealed.transaction.fee();
    113         if matches!(revealed.transaction, Transaction::Mine { .. }) {
    114             if let Some(commit) = blinded_by_commitment.get(&revealed.commitment) {
    115                 index_blinded_collateral_change(&mut outputs, commit, fee);
    116             }
    117         }
    118         if fee > 0 {
    119             let committer_fee = blinded_fee_share(fee, BLINDED_COMMITTER_FEE_BPS);
    120             if committer_fee > 0 {
    121                 outputs.insert(
    122                     blinded_committer_fee_outpoint(&revealed.commitment),
    123                     TxOutput {
    124                         address: revealed.included_by,
    125                         amount: committer_fee,
    126                     },
    127                 );
    128             }
    129             if let Some(block) = blocks_by_height.get(&revealed.height) {
    130                 let reveal_finalizer_fee = blinded_reveal_finalizer_fee(
    131                     fee,
    132                     block.included_reveal_bundle_count(),
    133                     reveal_bundle_slots_by_height
    134                         .get(&revealed.height)
    135                         .copied()
    136                         .unwrap_or(REVEAL_COMMITTEE_SIZE),
    137                 );
    138                 if reveal_finalizer_fee > 0 {
    139                     outputs.insert(
    140                         blinded_executor_fee_outpoint(&revealed.commitment),
    141                         TxOutput {
    142                             address: block.miner.clone(),
    143                             amount: reveal_finalizer_fee,
    144                         },
    145                     );
    146                 }
    147                 let reveal_bundle_signer_fee =
    148                     blinded_fee_share(fee, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS);
    149                 if reveal_bundle_signer_fee > 0 {
    150                     for signature in &block.reveal_bundle_section.signatures {
    151                         outputs.insert(
    152                             blinded_reveal_bundle_signer_fee_outpoint(
    153                                 &revealed.commitment,
    154                                 signature.slot,
    155                             ),
    156                             TxOutput {
    157                                 address: signature.member.clone(),
    158                                 amount: reveal_bundle_signer_fee,
    159                             },
    160                         );
    161                     }
    162                 }
    163             }
    164         }
    165     }
    166     index_expired_blinded_outputs(&mut outputs, snapshot);
    167     outputs
    168 }
    169 
    170 fn reveal_bundle_slots_by_height(snapshot: &ChainSnapshot) -> BTreeMap<u64, usize> {
    171     Ledger::from_persisted_snapshot(snapshot.clone())
    172         .ok()
    173         .and_then(|ledger| {
    174             ledger
    175                 .burn_leader_ranks_for_blocks(snapshot.blocks.iter().map(|block| block.height))
    176                 .ok()
    177         })
    178         .map(|ranks_by_height| {
    179             ranks_by_height
    180                 .into_iter()
    181                 .map(|(height, ranks)| {
    182                     (
    183                         height,
    184                         reveal_committee_slot_count_for_height(
    185                             height,
    186                             ranks.len(),
    187                             ranks.iter().map(|rank| rank.owner.as_str()),
    188                         ),
    189                     )
    190                 })
    191                 .collect()
    192         })
    193         .unwrap_or_default()
    194 }
    195 
    196 fn index_blinded_collateral_change(
    197     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    198     transaction: &BlindedTransaction,
    199     fee: Amount,
    200 ) {
    201     let Some(first_input) = transaction.inputs.first() else {
    202         return;
    203     };
    204     let locked_total = transaction.inputs.iter().fold(0_u64, |total, input| {
    205         total.saturating_add(
    206             outputs
    207                 .get(&input.outpoint)
    208                 .map(|output| output.amount)
    209                 .unwrap_or_default(),
    210         )
    211     });
    212     if fee >= locked_total {
    213         return;
    214     }
    215     outputs.insert(
    216         blinded_expiry_change_outpoint(&transaction.commitment),
    217         TxOutput {
    218             address: first_input.owner.clone(),
    219             amount: locked_total - fee,
    220         },
    221     );
    222 }
    223 
    224 fn index_expired_blinded_outputs(
    225     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    226     snapshot: &ChainSnapshot,
    227 ) {
    228     let mut active = BTreeMap::<String, (BlindedTransaction, Amount)>::new();
    229     for block in &snapshot.blocks {
    230         let revealed = block
    231             .all_blinded_reveals()
    232             .into_iter()
    233             .map(|reveal| reveal.commitment.clone())
    234             .collect::<BTreeSet<_>>();
    235         active.retain(|commitment, (transaction, locked_total)| {
    236             if revealed.contains(commitment) {
    237                 return false;
    238             }
    239             if block.height >= transaction.expires_at_height {
    240                 if let Some(first_input) = transaction.inputs.first() {
    241                     if transaction.fee <= *locked_total {
    242                         let change = *locked_total - transaction.fee;
    243                         if change > 0 {
    244                             outputs.insert(
    245                                 blinded_expiry_change_outpoint(commitment),
    246                                 TxOutput {
    247                                     address: first_input.owner.clone(),
    248                                     amount: change,
    249                                 },
    250                             );
    251                         }
    252                     }
    253                 }
    254                 return false;
    255             }
    256             true
    257         });
    258         for transaction in &block.blinded_transactions {
    259             let locked_total = transaction.inputs.iter().fold(0_u64, |total, input| {
    260                 total.saturating_add(
    261                     outputs
    262                         .get(&input.outpoint)
    263                         .map(|output| output.amount)
    264                         .unwrap_or_default(),
    265                 )
    266             });
    267             active.insert(
    268                 transaction.commitment.clone(),
    269                 (transaction.clone(), locked_total),
    270             );
    271         }
    272     }
    273 }
    274 
    275 fn index_transaction_outputs(
    276     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    277     transaction: &Transaction,
    278 ) {
    279     let created_outputs = match transaction {
    280         Transaction::Transfer { outputs, .. } => outputs.clone(),
    281         Transaction::Burn { change, .. } => change.clone(),
    282         Transaction::Mine { recipient, .. } => vec![TxOutput {
    283             address: recipient.clone(),
    284             amount: MINE_REWARD,
    285         }],
    286     };
    287     for (index, output) in created_outputs.iter().enumerate() {
    288         outputs.insert(
    289             OutPoint {
    290                 txid: transaction.signature().to_string(),
    291                 index: index as u32,
    292             },
    293             output.clone(),
    294         );
    295     }
    296 }
    297 
    298 fn genesis_allocation_outpoint(address: &str) -> OutPoint {
    299     OutPoint {
    300         txid: hex_hash(format!("iuna-genesis-allocation:{address}")),
    301         index: 0,
    302     }
    303 }
    304 
    305 fn reward_outpoint(block_hash: &str) -> OutPoint {
    306     OutPoint {
    307         txid: block_hash.to_string(),
    308         index: u32::MAX,
    309     }
    310 }
    311 
    312 fn blinded_committer_fee_outpoint(commitment: &str) -> OutPoint {
    313     OutPoint {
    314         txid: commitment.to_string(),
    315         index: u32::MAX - 1,
    316     }
    317 }
    318 
    319 fn blinded_executor_fee_outpoint(commitment: &str) -> OutPoint {
    320     OutPoint {
    321         txid: commitment.to_string(),
    322         index: u32::MAX - 2,
    323     }
    324 }
    325 
    326 fn blinded_reveal_bundle_signer_fee_outpoint(commitment: &str, slot: u8) -> OutPoint {
    327     OutPoint {
    328         txid: commitment.to_string(),
    329         index: u32::MAX - 3 - u32::from(slot),
    330     }
    331 }
    332 
    333 fn blinded_expiry_change_outpoint(commitment: &str) -> OutPoint {
    334     OutPoint {
    335         txid: commitment.to_string(),
    336         index: 0,
    337     }
    338 }
    339 
    340 fn blinded_fee_share(fee: Amount, bps: u64) -> Amount {
    341     ((fee as u128 * bps as u128) / BLINDED_FEE_BPS_DENOMINATOR as u128) as Amount
    342 }