iuna

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

ui_index.rs (11418B)


      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,
      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)| (height, reveal_committee_slot_count(ranks.len())))
    182                 .collect()
    183         })
    184         .unwrap_or_default()
    185 }
    186 
    187 fn index_blinded_collateral_change(
    188     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    189     transaction: &BlindedTransaction,
    190     fee: Amount,
    191 ) {
    192     let Some(first_input) = transaction.inputs.first() else {
    193         return;
    194     };
    195     let locked_total = transaction.inputs.iter().fold(0_u64, |total, input| {
    196         total.saturating_add(
    197             outputs
    198                 .get(&input.outpoint)
    199                 .map(|output| output.amount)
    200                 .unwrap_or_default(),
    201         )
    202     });
    203     if fee >= locked_total {
    204         return;
    205     }
    206     outputs.insert(
    207         blinded_expiry_change_outpoint(&transaction.commitment),
    208         TxOutput {
    209             address: first_input.owner.clone(),
    210             amount: locked_total - fee,
    211         },
    212     );
    213 }
    214 
    215 fn index_expired_blinded_outputs(
    216     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    217     snapshot: &ChainSnapshot,
    218 ) {
    219     let mut active = BTreeMap::<String, (BlindedTransaction, Amount)>::new();
    220     for block in &snapshot.blocks {
    221         let revealed = block
    222             .all_blinded_reveals()
    223             .into_iter()
    224             .map(|reveal| reveal.commitment.clone())
    225             .collect::<BTreeSet<_>>();
    226         active.retain(|commitment, (transaction, locked_total)| {
    227             if revealed.contains(commitment) {
    228                 return false;
    229             }
    230             if block.height >= transaction.expires_at_height {
    231                 if let Some(first_input) = transaction.inputs.first() {
    232                     if transaction.fee <= *locked_total {
    233                         let change = *locked_total - transaction.fee;
    234                         if change > 0 {
    235                             outputs.insert(
    236                                 blinded_expiry_change_outpoint(commitment),
    237                                 TxOutput {
    238                                     address: first_input.owner.clone(),
    239                                     amount: change,
    240                                 },
    241                             );
    242                         }
    243                     }
    244                 }
    245                 return false;
    246             }
    247             true
    248         });
    249         for transaction in &block.blinded_transactions {
    250             let locked_total = transaction.inputs.iter().fold(0_u64, |total, input| {
    251                 total.saturating_add(
    252                     outputs
    253                         .get(&input.outpoint)
    254                         .map(|output| output.amount)
    255                         .unwrap_or_default(),
    256                 )
    257             });
    258             active.insert(
    259                 transaction.commitment.clone(),
    260                 (transaction.clone(), locked_total),
    261             );
    262         }
    263     }
    264 }
    265 
    266 fn index_transaction_outputs(
    267     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    268     transaction: &Transaction,
    269 ) {
    270     let created_outputs = match transaction {
    271         Transaction::Transfer { outputs, .. } => outputs.clone(),
    272         Transaction::Burn { change, .. } => change.clone(),
    273         Transaction::Mine { recipient, .. } => vec![TxOutput {
    274             address: recipient.clone(),
    275             amount: MINE_REWARD,
    276         }],
    277     };
    278     for (index, output) in created_outputs.iter().enumerate() {
    279         outputs.insert(
    280             OutPoint {
    281                 txid: transaction.signature().to_string(),
    282                 index: index as u32,
    283             },
    284             output.clone(),
    285         );
    286     }
    287 }
    288 
    289 fn genesis_allocation_outpoint(address: &str) -> OutPoint {
    290     OutPoint {
    291         txid: hex_hash(format!("iuna-genesis-allocation:{address}")),
    292         index: 0,
    293     }
    294 }
    295 
    296 fn reward_outpoint(block_hash: &str) -> OutPoint {
    297     OutPoint {
    298         txid: block_hash.to_string(),
    299         index: u32::MAX,
    300     }
    301 }
    302 
    303 fn blinded_committer_fee_outpoint(commitment: &str) -> OutPoint {
    304     OutPoint {
    305         txid: commitment.to_string(),
    306         index: u32::MAX - 1,
    307     }
    308 }
    309 
    310 fn blinded_executor_fee_outpoint(commitment: &str) -> OutPoint {
    311     OutPoint {
    312         txid: commitment.to_string(),
    313         index: u32::MAX - 2,
    314     }
    315 }
    316 
    317 fn blinded_reveal_bundle_signer_fee_outpoint(commitment: &str, slot: u8) -> OutPoint {
    318     OutPoint {
    319         txid: commitment.to_string(),
    320         index: u32::MAX - 3 - u32::from(slot),
    321     }
    322 }
    323 
    324 fn blinded_expiry_change_outpoint(commitment: &str) -> OutPoint {
    325     OutPoint {
    326         txid: commitment.to_string(),
    327         index: 0,
    328     }
    329 }
    330 
    331 fn blinded_fee_share(fee: Amount, bps: u64) -> Amount {
    332     ((fee as u128 * bps as u128) / BLINDED_FEE_BPS_DENOMINATOR as u128) as Amount
    333 }