helpers.rs (5612B)
1 use std::collections::{BTreeMap, BTreeSet}; 2 3 use anyhow::{Context, Result, bail}; 4 use sha2::{Digest, Sha256}; 5 6 use crate::domain::{Amount, MINE_REWARD, OutPoint, Transaction}; 7 8 use super::FeeEstimate; 9 10 pub(super) fn auto_pow_salt(wallet_address: &str, anchor: &str) -> u64 { 11 let digest = Sha256::digest(format!("iuna-auto-pow:{wallet_address}:{anchor}").as_bytes()); 12 let mut bytes = [0_u8; 8]; 13 bytes.copy_from_slice(&digest[..8]); 14 u64::from_be_bytes(bytes) 15 } 16 17 pub(super) fn converge_fee_by_byte( 18 fee_per_byte: Amount, 19 mut build: impl FnMut(Amount) -> Result<Transaction>, 20 ) -> Result<(Transaction, FeeEstimate)> { 21 let mut fee = 1; 22 let mut best = None; 23 for _ in 0..64 { 24 let built = build(fee)?; 25 let bytes = built.economic_size_bytes(); 26 let required_fee = fee_per_byte 27 .checked_mul(bytes as Amount) 28 .context("fee per byte times transaction bytes overflows")? 29 .max(1); 30 if fee == required_fee { 31 return Ok((built, FeeEstimate { bytes, fee })); 32 } 33 if fee > required_fee 34 && best 35 .as_ref() 36 .is_none_or(|(_, estimate): &(Transaction, FeeEstimate)| fee < estimate.fee) 37 { 38 best = Some((built, FeeEstimate { bytes, fee })); 39 } 40 fee = required_fee; 41 } 42 43 // Transaction size can oscillate when a new fee selects a different UTXO. 44 // A previously built candidate whose fee covers its own size is already 45 // valid, even when the final iteration happens to underpay. 46 if let Some(best) = best { 47 return Ok(best); 48 } 49 50 let built = build(fee)?; 51 let bytes = built.economic_size_bytes(); 52 let required_fee = fee_per_byte 53 .checked_mul(bytes as Amount) 54 .context("fee per byte times transaction bytes overflows")? 55 .max(1); 56 if fee >= required_fee { 57 if best 58 .as_ref() 59 .is_none_or(|(_, estimate): &(Transaction, FeeEstimate)| fee < estimate.fee) 60 { 61 best = Some((built, FeeEstimate { bytes, fee })); 62 } 63 if let Some(best) = best { 64 return Ok(best); 65 } 66 } 67 let built = build(required_fee)?; 68 let bytes = built.economic_size_bytes(); 69 let final_required_fee = fee_per_byte 70 .checked_mul(bytes as Amount) 71 .context("fee per byte times transaction bytes overflows")? 72 .max(1); 73 if required_fee < final_required_fee { 74 bail!("fee per byte did not converge"); 75 } 76 Ok(( 77 built, 78 FeeEstimate { 79 bytes, 80 fee: required_fee, 81 }, 82 )) 83 } 84 85 pub(super) fn transaction_output_total_for_address( 86 transaction: &Transaction, 87 address: &str, 88 ) -> Amount { 89 match transaction { 90 Transaction::Transfer { outputs, .. } => outputs, 91 Transaction::Burn { change, .. } => change, 92 Transaction::Mine { recipient, .. } if recipient == address => return MINE_REWARD, 93 Transaction::Mine { .. } => return 0, 94 } 95 .iter() 96 .filter(|output| output.address == address) 97 .fold(0_u64, |total, output| total.saturating_add(output.amount)) 98 } 99 100 pub(super) fn transaction_input_total_from_outputs( 101 transaction: &Transaction, 102 address: &str, 103 outputs: &BTreeMap<OutPoint, Amount>, 104 ) -> Amount { 105 let inputs = match transaction { 106 Transaction::Transfer { inputs, .. } | Transaction::Burn { inputs, .. } => inputs, 107 Transaction::Mine { .. } => return 0, 108 }; 109 inputs 110 .iter() 111 .filter(|input| input.owner == address) 112 .filter_map(|input| outputs.get(&input.outpoint)) 113 .fold(0_u64, |total, amount| total.saturating_add(*amount)) 114 } 115 116 pub(super) fn transaction_input_outpoints(transaction: &Transaction) -> BTreeSet<OutPoint> { 117 match transaction { 118 Transaction::Transfer { inputs, .. } | Transaction::Burn { inputs, .. } => inputs, 119 Transaction::Mine { .. } => return BTreeSet::new(), 120 } 121 .iter() 122 .map(|input| input.outpoint.clone()) 123 .collect() 124 } 125 126 pub(super) fn allowed_fallback_vdf_rank_count(rank_count: usize, percent: u8) -> usize { 127 if rank_count == 0 || percent == 0 { 128 return 0; 129 } 130 rank_count 131 .saturating_mul(usize::from(percent.min(100))) 132 .saturating_add(99) 133 / 100 134 } 135 136 #[cfg(test)] 137 mod tests { 138 use crate::domain::{OutPoint, Transaction, TxInput}; 139 140 use super::converge_fee_by_byte; 141 142 fn burn_with_inputs(fee: u64, input_count: usize) -> Transaction { 143 Transaction::Burn { 144 inputs: (0..input_count) 145 .map(|index| TxInput { 146 outpoint: OutPoint { 147 txid: format!("{index:064x}"), 148 index: index as u32, 149 }, 150 owner: "a".repeat(64), 151 signature: "b".repeat(128), 152 }) 153 .collect(), 154 change: Vec::new(), 155 amount: 1, 156 fee, 157 anchor: None, 158 signature: "c".repeat(128), 159 } 160 } 161 162 #[test] 163 fn fee_convergence_keeps_valid_candidate_when_transaction_size_oscillates() { 164 let large_size = burn_with_inputs(1, 2).economic_size_bytes() as u64; 165 let (transaction, estimate) = converge_fee_by_byte(1, |fee| { 166 let input_count = if fee < large_size { 2 } else { 1 }; 167 Ok(burn_with_inputs(fee, input_count)) 168 }) 169 .unwrap(); 170 171 assert_eq!(estimate.fee, transaction.fee()); 172 assert!(estimate.fee >= transaction.economic_size_bytes() as u64); 173 } 174 }