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 }