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 }