block.rs (11456B)
1 use std::collections::BTreeMap; 2 3 use anyhow::{Context, Result}; 4 use serde::{Deserialize, Serialize}; 5 6 use super::{ 7 Amount, BlindedReveal, BlindedTransaction, BurnTicket, LaunchProfile, LeaderScore, 8 REVEAL_COMMITTEE_SIZE, RevealBundleSection, Transaction, Wallet, hex_hash, 9 recovery_vdf_seed_for_child, vdf_seed_for_child, 10 }; 11 12 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 13 pub struct Block { 14 pub height: u64, 15 pub prev_hash: String, 16 pub timestamp_ms: u64, 17 pub miner: String, 18 #[serde(default)] 19 pub finalizer_mode: FinalizerMode, 20 #[serde(default)] 21 pub finalizer_rank: u32, 22 pub reward: Amount, 23 pub vdf_rounds: u64, 24 pub vdf_output: String, 25 pub leader_proof: Option<LeaderProof>, 26 #[serde(default)] 27 pub blinded_transactions: Vec<BlindedTransaction>, 28 #[serde(default)] 29 pub reveal_bundle_section: RevealBundleSection, 30 pub transactions: Vec<Transaction>, 31 pub hash: String, 32 } 33 34 #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 35 #[serde(rename_all = "snake_case")] 36 pub enum FinalizerMode { 37 #[default] 38 Ticket, 39 Recovery, 40 } 41 42 impl Block { 43 pub fn compute_hash(&self) -> String { 44 hex_hash(format!( 45 "block:{}:{}:{}", 46 self.content_hash(), 47 self.vdf_seed(), 48 self.vdf_output, 49 )) 50 } 51 52 pub fn vdf_seed(&self) -> String { 53 let bundle_hashes = self.reveal_bundle_hashes(); 54 match self.finalizer_mode { 55 FinalizerMode::Ticket => { 56 vdf_seed_for_child(&self.prev_hash, self.height, &bundle_hashes) 57 } 58 FinalizerMode::Recovery => recovery_vdf_seed_for_child( 59 &self.prev_hash, 60 self.height, 61 self.timestamp_ms, 62 &bundle_hashes, 63 ), 64 } 65 } 66 67 fn content_hash(&self) -> String { 68 let txs = self 69 .transactions 70 .iter() 71 .map(Transaction::canonical) 72 .collect::<Vec<_>>() 73 .join("|"); 74 let blinded = self 75 .blinded_transactions 76 .iter() 77 .map(BlindedTransaction::canonical) 78 .collect::<Vec<_>>() 79 .join("|"); 80 let reveal_section = self.reveal_bundle_section.canonical(); 81 let leader_proof = self 82 .leader_proof 83 .as_ref() 84 .map(|proof| { 85 format!( 86 "{}:{}:{}", 87 proof.ticket_id, proof.public_key, proof.signature 88 ) 89 }) 90 .unwrap_or_default(); 91 hex_hash(format!( 92 "block-content-v3:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", 93 self.height, 94 self.prev_hash, 95 self.timestamp_ms, 96 self.miner, 97 self.finalizer_rank, 98 self.reward, 99 self.vdf_rounds, 100 leader_proof, 101 txs, 102 canonical_blinded_block_items(&blinded, &reveal_section) 103 )) 104 } 105 106 pub(super) fn leader_score(&self) -> LeaderScore { 107 LeaderScore { 108 finalizer_mode_rank: self.finalizer_mode.fork_choice_rank(), 109 finalizer_rank: self.finalizer_rank, 110 proof_rank: self 111 .leader_proof 112 .as_ref() 113 .map(LeaderProof::rank) 114 .unwrap_or_else(|| self.hash.clone()), 115 } 116 } 117 118 pub fn serialized_size_bytes(&self) -> Result<usize> { 119 serde_json::to_vec(self) 120 .map(|bytes| bytes.len()) 121 .context("failed to serialize block for size check") 122 } 123 124 pub fn all_blinded_reveals(&self) -> Vec<&BlindedReveal> { 125 self.reveal_bundle_section.all_reveals() 126 } 127 128 pub fn reveal_bundle_hashes(&self) -> [String; REVEAL_COMMITTEE_SIZE] { 129 self.reveal_bundle_section 130 .reveal_bundle_hashes(self.height, &self.prev_hash) 131 } 132 133 pub fn included_reveal_bundle_count(&self) -> usize { 134 self.reveal_bundle_section.included_bundle_count() 135 } 136 } 137 138 impl FinalizerMode { 139 fn fork_choice_rank(self) -> u8 { 140 match self { 141 Self::Ticket => 0, 142 Self::Recovery => 1, 143 } 144 } 145 } 146 147 fn canonical_blinded_block_items(blinded: &str, reveal_section: &str) -> String { 148 format!("blinded-v3:{blinded}:reveal-section:{reveal_section}") 149 } 150 151 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 152 pub struct LeaderProof { 153 pub ticket_id: String, 154 pub public_key: String, 155 pub signature: String, 156 } 157 158 impl LeaderProof { 159 fn rank(&self) -> String { 160 hex_hash(format!( 161 "iuna-leader-rank:{}:{}", 162 self.ticket_id, self.signature 163 )) 164 } 165 } 166 167 #[derive(Clone, Debug, Eq, PartialEq)] 168 pub(super) struct LeaderProofPayload { 169 pub(super) height: u64, 170 pub(super) prev_hash: String, 171 pub(super) finalizer_rank: u32, 172 pub(super) vdf_output: String, 173 pub(super) ticket_id: String, 174 pub(super) ticket_amount: Amount, 175 pub(super) ticket_owner: String, 176 } 177 178 impl LeaderProofPayload { 179 pub(super) fn canonical(&self) -> String { 180 if self.finalizer_rank == 0 { 181 format!( 182 "iuna-leader-proof:{}:{}:{}:{}:{}:{}", 183 self.height, 184 self.prev_hash, 185 self.vdf_output, 186 self.ticket_id, 187 self.ticket_amount, 188 self.ticket_owner 189 ) 190 } else { 191 format!( 192 "iuna-leader-proof-v2:{}:{}:{}:{}:{}:{}:{}", 193 self.height, 194 self.prev_hash, 195 self.finalizer_rank, 196 self.vdf_output, 197 self.ticket_id, 198 self.ticket_amount, 199 self.ticket_owner 200 ) 201 } 202 } 203 } 204 205 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 206 #[serde(rename_all = "camelCase")] 207 pub struct BurnLeaderRank { 208 pub rank: u32, 209 pub ticket_id: String, 210 pub owner: String, 211 pub amount: Amount, 212 pub eligible_from_height: u64, 213 pub eligible_until_height: u64, 214 } 215 216 #[derive(Clone, Debug)] 217 pub struct PreparedBlock { 218 pub(super) height: u64, 219 pub(super) prev_hash: String, 220 pub(super) timestamp_ms: u64, 221 pub(super) miner: String, 222 pub(super) finalizer_mode: FinalizerMode, 223 pub(super) finalizer_rank: u32, 224 pub(super) reward: Amount, 225 pub(super) vdf_rounds: u64, 226 pub(super) vdf_seed: String, 227 pub(super) leader_ticket: Option<BurnTicket>, 228 pub(super) blinded_transactions: Vec<BlindedTransaction>, 229 pub(super) reveal_bundle_section: RevealBundleSection, 230 pub(super) transactions: Vec<Transaction>, 231 } 232 233 impl PreparedBlock { 234 pub fn vdf_seed(&self) -> &str { 235 &self.vdf_seed 236 } 237 238 pub fn vdf_rounds(&self) -> u64 { 239 self.vdf_rounds 240 } 241 242 pub fn height(&self) -> u64 { 243 self.height 244 } 245 246 pub fn timestamp_ms(&self) -> u64 { 247 self.timestamp_ms 248 } 249 250 pub fn finish(self, wallet: &Wallet, vdf_output: String) -> Block { 251 let timestamp_ms = self.timestamp_ms; 252 self.finish_with_timestamp(wallet, vdf_output, timestamp_ms) 253 } 254 255 pub fn finish_at(self, wallet: &Wallet, vdf_output: String, timestamp_ms: u64) -> Block { 256 let timestamp_ms = match self.finalizer_mode { 257 FinalizerMode::Ticket => timestamp_ms.max(self.timestamp_ms), 258 FinalizerMode::Recovery => self.timestamp_ms, 259 }; 260 self.finish_with_timestamp(wallet, vdf_output, timestamp_ms) 261 } 262 263 fn finish_with_timestamp( 264 self, 265 wallet: &Wallet, 266 vdf_output: String, 267 timestamp_ms: u64, 268 ) -> Block { 269 let leader_proof = self.leader_ticket.as_ref().map(|leader_ticket| { 270 let proof_payload = LeaderProofPayload { 271 height: self.height, 272 prev_hash: self.prev_hash.clone(), 273 finalizer_rank: self.finalizer_rank, 274 vdf_output: vdf_output.clone(), 275 ticket_id: leader_ticket.id.clone(), 276 ticket_amount: leader_ticket.amount, 277 ticket_owner: leader_ticket.owner.clone(), 278 }; 279 wallet.leader_proof(&proof_payload) 280 }); 281 let mut block = Block { 282 height: self.height, 283 prev_hash: self.prev_hash, 284 timestamp_ms, 285 miner: self.miner, 286 finalizer_mode: self.finalizer_mode, 287 finalizer_rank: self.finalizer_rank, 288 reward: self.reward, 289 vdf_rounds: self.vdf_rounds, 290 vdf_output, 291 leader_proof, 292 blinded_transactions: self.blinded_transactions, 293 reveal_bundle_section: self.reveal_bundle_section, 294 transactions: self.transactions, 295 hash: String::new(), 296 }; 297 block.hash = block.compute_hash(); 298 block 299 } 300 } 301 302 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 303 pub struct ChainStatus { 304 pub height: u64, 305 pub tip_hash: String, 306 pub next_leader: Option<String>, 307 pub launch_profile_hash: String, 308 pub mine_reward: Amount, 309 pub current_mine_difficulty_bits: u32, 310 pub balances: BTreeMap<String, Amount>, 311 pub pending_transactions: usize, 312 } 313 314 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 315 pub struct ChainSnapshot { 316 pub genesis_allocations: BTreeMap<String, Amount>, 317 pub vdf_rounds: u64, 318 pub launch_profile: LaunchProfile, 319 pub blocks: Vec<Block>, 320 } 321 322 #[cfg(test)] 323 mod tests { 324 use super::{Block, FinalizerMode, LeaderProofPayload, canonical_blinded_block_items}; 325 use crate::domain::{RevealBundleSection, Transaction}; 326 327 #[test] 328 fn primary_leader_proof_payload_omits_rank_from_canonical_form() { 329 let primary = LeaderProofPayload { 330 height: 1, 331 prev_hash: "prev".to_string(), 332 finalizer_rank: 0, 333 vdf_output: "vdf".to_string(), 334 ticket_id: "ticket".to_string(), 335 ticket_amount: 2, 336 ticket_owner: "owner".to_string(), 337 }; 338 let fallback = LeaderProofPayload { 339 finalizer_rank: 1, 340 ..primary.clone() 341 }; 342 343 assert_eq!( 344 primary.canonical(), 345 "iuna-leader-proof:1:prev:vdf:ticket:2:owner" 346 ); 347 assert_eq!( 348 fallback.canonical(), 349 "iuna-leader-proof-v2:1:prev:1:vdf:ticket:2:owner" 350 ); 351 } 352 353 #[test] 354 fn blinded_block_items_keep_canonical_prefix() { 355 assert_eq!( 356 canonical_blinded_block_items("blind", "section"), 357 "blinded-v3:blind:reveal-section:section" 358 ); 359 } 360 361 #[test] 362 fn block_compute_hash_sets_content_and_vdf_seed_contract() { 363 let mut block = Block { 364 height: 1, 365 prev_hash: "0".repeat(64), 366 timestamp_ms: 1, 367 miner: "miner".to_string(), 368 finalizer_mode: FinalizerMode::Ticket, 369 finalizer_rank: 0, 370 reward: 0, 371 vdf_rounds: 1, 372 vdf_output: "out".to_string(), 373 leader_proof: None, 374 blinded_transactions: Vec::new(), 375 reveal_bundle_section: RevealBundleSection::default(), 376 transactions: vec![Transaction::genesis_burn("owner", 1)], 377 hash: String::new(), 378 }; 379 380 block.hash = block.compute_hash(); 381 382 assert_eq!(block.compute_hash(), block.hash); 383 } 384 }