ledger_prepare.rs (8670B)
1 use anyhow::{Result, bail}; 2 3 use super::{ 4 BurnBundle, FinalizerMode, Ledger, PreparedBlock, RECOVERY_BLOCK_DELAY_MS, Wallet, 5 ensure_block_has_burn, ensure_block_has_burn_from, recovery_vdf_seed_for_child, run_vdf, 6 ticket_block_min_timestamp, vdf_content_commitment, vdf_seed_for_child, 7 }; 8 9 impl Ledger { 10 pub fn mine_next_block(&self, wallet: &Wallet, timestamp_ms: u64) -> Result<super::Block> { 11 let next_height = self.height().saturating_add(1); 12 let reward_address = (next_height >= super::HYBRID_REWARD_ACTIVATION_HEIGHT) 13 .then(|| self.wallet_reward_address(wallet, next_height)); 14 let prepared = self.prepare_next_block_with_required_burn_and_burn_bundles( 15 wallet.address(), 16 reward_address.as_deref(), 17 timestamp_ms, 18 Vec::new(), 19 None, 20 )?; 21 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 22 Ok(prepared.finish(wallet, vdf_output)) 23 } 24 25 pub fn mine_recovery_block(&self, wallet: &Wallet, timestamp_ms: u64) -> Result<super::Block> { 26 let next_height = self.height().saturating_add(1); 27 let reward_address = (next_height >= super::HYBRID_REWARD_ACTIVATION_HEIGHT) 28 .then(|| self.wallet_reward_address(wallet, next_height)); 29 let prepared = self.prepare_recovery_block_with_required_burn_and_burn_bundles( 30 wallet.address(), 31 reward_address.as_deref(), 32 timestamp_ms, 33 Vec::new(), 34 None, 35 )?; 36 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 37 Ok(prepared.finish(wallet, vdf_output)) 38 } 39 40 pub fn prepare_next_block(&self, miner: &str, timestamp_ms: u64) -> Result<PreparedBlock> { 41 self.prepare_next_block_with_burn_bundles(miner, timestamp_ms, Vec::new()) 42 } 43 44 pub fn prepare_next_block_with_burn_bundles( 45 &self, 46 miner: &str, 47 timestamp_ms: u64, 48 burn_bundles: Vec<BurnBundle>, 49 ) -> Result<PreparedBlock> { 50 self.prepare_next_block_with_required_burn_and_burn_bundles( 51 miner, 52 None, 53 timestamp_ms, 54 burn_bundles, 55 None, 56 ) 57 } 58 59 pub(crate) fn prepare_next_block_with_required_burn_and_burn_bundles( 60 &self, 61 miner: &str, 62 reward_address: Option<&str>, 63 timestamp_ms: u64, 64 burn_bundles: Vec<BurnBundle>, 65 required_burn_signature: Option<&str>, 66 ) -> Result<PreparedBlock> { 67 let height = self.tip().height + 1; 68 self.validate_reward_address(height, reward_address, "finalizer")?; 69 let Some((finalizer_rank, leader_ticket)) = self.finalizer_ticket_for_miner(height, miner) 70 else { 71 bail!("cannot mine block without a mature burn ticket"); 72 }; 73 if self.expected_leader_for_next_block().is_none() { 74 bail!("no selected leader for block {height}"); 75 } 76 77 let burn_bundles = 78 self.validate_next_block_burn_bundles_for_finalizer_rank(finalizer_rank, burn_bundles)?; 79 let burn_bundle_section = self.burn_bundle_section_from_bundles(burn_bundles)?; 80 let selection = self.select_block_transactions_with_burn_section( 81 miner, 82 required_burn_signature, 83 &burn_bundle_section, 84 )?; 85 ensure_block_has_burn(&selection.transactions, &selection.transactions_v2)?; 86 87 let tip = self.tip(); 88 let prev_hash = tip.hash.clone(); 89 let timestamp_ms = timestamp_ms.max(ticket_block_min_timestamp(tip, finalizer_rank)?); 90 let bundle_hashes = burn_bundle_section.burn_bundle_hashes(height, &prev_hash, miner); 91 let reward = self.expected_reward_for_next_block( 92 &selection.transactions, 93 &selection.transactions_v2, 94 &burn_bundle_section, 95 )?; 96 let vdf_rounds = self.vdf_rounds_for_finalizer_rank(finalizer_rank)?; 97 let content_commitment = vdf_content_commitment( 98 height, 99 &prev_hash, 100 miner, 101 reward_address, 102 FinalizerMode::Ticket, 103 finalizer_rank, 104 reward, 105 vdf_rounds, 106 Some(&leader_ticket.id), 107 &burn_bundle_section, 108 &selection.transactions, 109 &selection.transactions_v2, 110 ); 111 let vdf_seed = vdf_seed_for_child(&prev_hash, height, &bundle_hashes, &content_commitment); 112 Ok(PreparedBlock { 113 height, 114 prev_hash, 115 timestamp_ms, 116 miner: miner.to_string(), 117 reward_address: reward_address.map(str::to_string), 118 finalizer_mode: FinalizerMode::Ticket, 119 reward, 120 vdf_rounds, 121 vdf_seed, 122 finalizer_rank, 123 leader_ticket: Some(leader_ticket), 124 burn_bundle_section, 125 transactions: selection.transactions, 126 transactions_v2: selection.transactions_v2, 127 }) 128 } 129 130 pub fn recovery_block_available_at(&self, timestamp_ms: u64) -> bool { 131 timestamp_ms >= self.recovery_block_min_timestamp() 132 } 133 134 pub fn recovery_block_min_timestamp(&self) -> u64 { 135 self.tip() 136 .timestamp_ms 137 .saturating_add(RECOVERY_BLOCK_DELAY_MS) 138 } 139 140 pub fn prepare_recovery_block(&self, miner: &str, timestamp_ms: u64) -> Result<PreparedBlock> { 141 self.prepare_recovery_block_with_burn_bundles(miner, timestamp_ms, Vec::new()) 142 } 143 144 pub fn prepare_recovery_block_with_burn_bundles( 145 &self, 146 miner: &str, 147 timestamp_ms: u64, 148 burn_bundles: Vec<BurnBundle>, 149 ) -> Result<PreparedBlock> { 150 self.prepare_recovery_block_with_required_burn_and_burn_bundles( 151 miner, 152 None, 153 timestamp_ms, 154 burn_bundles, 155 None, 156 ) 157 } 158 159 pub(crate) fn prepare_recovery_block_with_required_burn_and_burn_bundles( 160 &self, 161 miner: &str, 162 reward_address: Option<&str>, 163 timestamp_ms: u64, 164 burn_bundles: Vec<BurnBundle>, 165 required_burn_signature: Option<&str>, 166 ) -> Result<PreparedBlock> { 167 let height = self.tip().height + 1; 168 self.validate_reward_address(height, reward_address, "finalizer")?; 169 let min_timestamp = self.recovery_block_min_timestamp(); 170 if timestamp_ms < min_timestamp { 171 bail!("recovery block is not available before timestamp {min_timestamp}"); 172 } 173 174 let burn_bundles = self.validate_next_block_burn_bundles(burn_bundles)?; 175 let burn_bundle_section = self.burn_bundle_section_from_bundles(burn_bundles)?; 176 let selection = self.select_recovery_block_transactions_with_burn_section( 177 miner, 178 required_burn_signature, 179 &burn_bundle_section, 180 )?; 181 ensure_block_has_burn(&selection.transactions, &selection.transactions_v2)?; 182 ensure_block_has_burn_from(&selection.transactions, &selection.transactions_v2, miner)?; 183 184 let tip = self.tip(); 185 let prev_hash = tip.hash.clone(); 186 let timestamp_ms = timestamp_ms.max(tip.timestamp_ms + 1); 187 let bundle_hashes = burn_bundle_section.burn_bundle_hashes(height, &prev_hash, miner); 188 let reward = self.expected_reward_for_next_block( 189 &selection.transactions, 190 &selection.transactions_v2, 191 &burn_bundle_section, 192 )?; 193 let vdf_rounds = self.recovery_vdf_rounds()?; 194 let content_commitment = vdf_content_commitment( 195 height, 196 &prev_hash, 197 miner, 198 reward_address, 199 FinalizerMode::Recovery, 200 0, 201 reward, 202 vdf_rounds, 203 None, 204 &burn_bundle_section, 205 &selection.transactions, 206 &selection.transactions_v2, 207 ); 208 let vdf_seed = recovery_vdf_seed_for_child( 209 &prev_hash, 210 height, 211 timestamp_ms, 212 &bundle_hashes, 213 &content_commitment, 214 ); 215 Ok(PreparedBlock { 216 height, 217 prev_hash, 218 timestamp_ms, 219 miner: miner.to_string(), 220 reward_address: reward_address.map(str::to_string), 221 finalizer_mode: FinalizerMode::Recovery, 222 finalizer_rank: 0, 223 reward, 224 vdf_rounds, 225 vdf_seed, 226 leader_ticket: None, 227 burn_bundle_section, 228 transactions: selection.transactions, 229 transactions_v2: selection.transactions_v2, 230 }) 231 } 232 }