iuna

iuna

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

genesis.rs (6106B)


      1 use std::collections::BTreeMap;
      2 
      3 use anyhow::{Result, bail};
      4 
      5 use super::{
      6     Amount, BLOCK_REWARD, Block, BurnBundleSection, FinalizerMode, OutPoint, Transaction,
      7     TransactionSigningDomain, TxOutput, apply_transaction, credit_reward_output, hex_hash,
      8     validate_genesis_burn_transaction,
      9 };
     10 
     11 pub(super) fn build_genesis_block(
     12     genesis_allocations: &BTreeMap<String, Amount>,
     13     transactions: Vec<Transaction>,
     14 ) -> Block {
     15     let miner = genesis_miner(genesis_allocations, &transactions);
     16     let reward = genesis_reward(genesis_allocations, &transactions);
     17     let txs = transactions
     18         .iter()
     19         .map(Transaction::canonical)
     20         .collect::<Vec<_>>()
     21         .join("|");
     22     let vdf_output = hex_hash(format!("iuna-genesis-vdf:{genesis_allocations:?}:{txs}"));
     23     let mut genesis = Block {
     24         height: 0,
     25         prev_hash: "0".repeat(64),
     26         timestamp_ms: 0,
     27         miner,
     28         reward_address: None,
     29         reward_address_signature: None,
     30         finalizer_mode: FinalizerMode::Ticket,
     31         finalizer_rank: 0,
     32         reward,
     33         vdf_rounds: 0,
     34         vdf_output,
     35         leader_proof: None,
     36         burn_bundle_section: BurnBundleSection::default(),
     37         transactions,
     38         transactions_v2: Vec::new(),
     39         hash: String::new(),
     40     };
     41     genesis.hash = genesis.compute_hash();
     42     genesis
     43 }
     44 
     45 pub(super) fn utxos_after_genesis(
     46     genesis_allocations: &BTreeMap<String, Amount>,
     47     genesis: &Block,
     48 ) -> Result<BTreeMap<OutPoint, TxOutput>> {
     49     let mut utxos = genesis_allocation_utxos(genesis_allocations);
     50     let signing_domain = TransactionSigningDomain::legacy();
     51     for transaction in &genesis.transactions {
     52         match transaction {
     53             Transaction::Burn { .. } => {
     54                 validate_genesis_burn_transaction(transaction)?;
     55                 apply_transaction(transaction, &mut utxos, &signing_domain)?;
     56             }
     57             Transaction::Transfer { .. } | Transaction::Mine { .. } => {
     58                 bail!("genesis only supports burn transactions")
     59             }
     60         }
     61     }
     62     credit_reward_output(&mut utxos, genesis)?;
     63     Ok(utxos)
     64 }
     65 
     66 fn genesis_allocation_utxos(
     67     genesis_allocations: &BTreeMap<String, Amount>,
     68 ) -> BTreeMap<OutPoint, TxOutput> {
     69     genesis_allocations
     70         .iter()
     71         .filter(|(_, amount)| **amount > 0)
     72         .map(|(address, amount)| {
     73             (
     74                 genesis_allocation_outpoint(address),
     75                 TxOutput {
     76                     address: address.clone(),
     77                     amount: *amount,
     78                 },
     79             )
     80         })
     81         .collect()
     82 }
     83 
     84 pub(super) fn balances_from_utxos(
     85     utxos: &BTreeMap<OutPoint, TxOutput>,
     86 ) -> BTreeMap<String, Amount> {
     87     let mut balances = BTreeMap::new();
     88     for output in utxos.values() {
     89         let balance = balances.entry(output.address.clone()).or_insert(0_u64);
     90         *balance = balance.saturating_add(output.amount);
     91     }
     92     balances
     93 }
     94 
     95 pub(crate) fn genesis_allocation_outpoint(address: &str) -> OutPoint {
     96     OutPoint {
     97         txid: hex_hash(format!("iuna-genesis-allocation:{address}")),
     98         index: 0,
     99     }
    100 }
    101 
    102 pub(super) fn validate_genesis_block(block: &Block) -> Result<()> {
    103     if block.height != 0 {
    104         bail!("genesis block height must be 0");
    105     }
    106     if block.prev_hash != "0".repeat(64) {
    107         bail!("genesis block prev_hash must be all zeroes");
    108     }
    109     if block.timestamp_ms != 0 {
    110         bail!("genesis block timestamp must be 0");
    111     }
    112     if block.miner == "genesis" && block.reward != 0 {
    113         bail!("genesis placeholder miner must not receive a reward");
    114     }
    115     if block.miner != "genesis" && block.reward != 0 && block.reward != BLOCK_REWARD {
    116         bail!("genesis block reward is invalid");
    117     }
    118     if block.vdf_rounds != 0 {
    119         bail!("genesis block VDF rounds must be 0");
    120     }
    121     if block.leader_proof.is_some() {
    122         bail!("genesis block must not carry a leader proof");
    123     }
    124     if !block.burn_bundle_section.is_empty() {
    125         bail!("genesis block must not carry burn bundle attestations");
    126     }
    127     if block.compute_hash() != block.hash {
    128         bail!("genesis block hash is invalid");
    129     }
    130     Ok(())
    131 }
    132 
    133 fn genesis_miner(
    134     genesis_allocations: &BTreeMap<String, Amount>,
    135     transactions: &[Transaction],
    136 ) -> String {
    137     transactions
    138         .iter()
    139         .filter_map(|transaction| match transaction {
    140             Transaction::Burn { inputs, .. } => inputs.first().map(|input| input.owner.as_str()),
    141             Transaction::Transfer { .. } | Transaction::Mine { .. } => None,
    142         })
    143         .find(|from| genesis_allocations.contains_key(*from))
    144         .or_else(|| genesis_allocations.keys().next().map(String::as_str))
    145         .unwrap_or("genesis")
    146         .to_string()
    147 }
    148 
    149 fn genesis_reward(
    150     genesis_allocations: &BTreeMap<String, Amount>,
    151     transactions: &[Transaction],
    152 ) -> Amount {
    153     if genesis_allocations.is_empty() || transactions.is_empty() {
    154         0
    155     } else {
    156         BLOCK_REWARD
    157     }
    158 }
    159 
    160 #[cfg(test)]
    161 mod tests {
    162     use std::collections::BTreeMap;
    163 
    164     use super::{build_genesis_block, genesis_allocation_outpoint, validate_genesis_block};
    165     use crate::domain::{MICRO_IUNA, Transaction, Wallet};
    166 
    167     #[test]
    168     fn genesis_allocation_outpoint_is_address_bound() {
    169         let alice = Wallet::from_seed("genesis-outpoint-alice");
    170         let bob = Wallet::from_seed("genesis-outpoint-bob");
    171 
    172         assert_ne!(
    173             genesis_allocation_outpoint(alice.address()),
    174             genesis_allocation_outpoint(bob.address())
    175         );
    176         assert_eq!(genesis_allocation_outpoint(alice.address()).index, 0);
    177     }
    178 
    179     #[test]
    180     fn built_genesis_block_validates() {
    181         let wallet = Wallet::from_seed("genesis-module-validates");
    182         let mut allocations = BTreeMap::new();
    183         allocations.insert(wallet.address().to_string(), MICRO_IUNA);
    184         let burn = Transaction::genesis_burn(wallet.address(), MICRO_IUNA);
    185 
    186         let genesis = build_genesis_block(&allocations, vec![burn]);
    187 
    188         validate_genesis_block(&genesis).unwrap();
    189     }
    190 }