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 }