iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }