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