iuna

iuna

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

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 }