iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

reveal.rs (10037B)


      1 use anyhow::{Context, Result, bail};
      2 use serde::{Deserialize, Serialize};
      3 
      4 use super::{Amount, BURN_COMMITTEE_SIZE, Transaction, hex_hash};
      5 
      6 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
      7 #[serde(rename_all = "camelCase")]
      8 pub struct BurnBundle {
      9     pub height: u64,
     10     pub prev_hash: String,
     11     pub slot: u8,
     12     pub member: String,
     13     #[serde(default, skip_serializing_if = "Option::is_none")]
     14     pub reward_address: Option<String>,
     15     pub burns: Vec<Transaction>,
     16     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     17     pub burns_v2: Vec<String>,
     18     pub signature: String,
     19 }
     20 
     21 impl BurnBundle {
     22     pub fn canonical_payload(&self) -> String {
     23         BurnBundlePayload {
     24             height: self.height,
     25             prev_hash: self.prev_hash.clone(),
     26             slot: self.slot,
     27             member: self.member.clone(),
     28             reward_address: self.reward_address.clone(),
     29             burns: self.burns.clone(),
     30             burns_v2: self.burns_v2.clone(),
     31         }
     32         .canonical()
     33     }
     34 
     35     pub fn canonical(&self) -> String {
     36         format!("{}:{}", self.canonical_payload(), self.signature)
     37     }
     38 
     39     pub fn bundle_hash(&self) -> String {
     40         hex_hash(self.canonical())
     41     }
     42 
     43     pub fn serialized_size_bytes(&self) -> Result<usize> {
     44         serde_json::to_vec(self)
     45             .map(|bytes| bytes.len())
     46             .context("failed to serialize burn bundle for size check")
     47     }
     48 }
     49 
     50 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     51 #[serde(rename_all = "camelCase")]
     52 pub struct BurnBundleSignature {
     53     pub slot: u8,
     54     pub member: String,
     55     #[serde(default, skip_serializing_if = "Option::is_none")]
     56     pub reward_address: Option<String>,
     57     pub signature: String,
     58 }
     59 
     60 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     61 #[serde(rename_all = "camelCase")]
     62 pub struct MaskedBurn {
     63     pub burn: Transaction,
     64     pub bundle_mask: u8,
     65 }
     66 
     67 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     68 #[serde(rename_all = "camelCase")]
     69 pub struct MaskedBurnV2 {
     70     pub envelope: String,
     71     pub bundle_mask: u8,
     72 }
     73 
     74 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
     75 #[serde(rename_all = "camelCase")]
     76 pub struct BurnBundleSection {
     77     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     78     pub signatures: Vec<BurnBundleSignature>,
     79     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     80     pub burns: Vec<MaskedBurn>,
     81     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     82     pub burns_v2: Vec<MaskedBurnV2>,
     83 }
     84 
     85 impl BurnBundleSection {
     86     pub fn is_empty(&self) -> bool {
     87         self.signatures.is_empty() && self.burns.is_empty() && self.burns_v2.is_empty()
     88     }
     89 
     90     pub fn required_burns(&self) -> Vec<&Transaction> {
     91         self.burns.iter().map(|masked| &masked.burn).collect()
     92     }
     93 
     94     pub fn required_burns_v2(&self) -> Vec<&str> {
     95         self.burns_v2
     96             .iter()
     97             .map(|masked| masked.envelope.as_str())
     98             .collect()
     99     }
    100 
    101     pub fn included_bundle_count(&self) -> usize {
    102         self.signatures.len()
    103     }
    104 
    105     pub fn expand(&self, height: u64, prev_hash: &str) -> Vec<BurnBundle> {
    106         self.signatures
    107             .iter()
    108             .map(|signature| {
    109                 let slot_mask = burn_bundle_slot_mask(signature.slot).unwrap_or(0);
    110                 let burns = self
    111                     .burns
    112                     .iter()
    113                     .filter(|masked| masked.bundle_mask & slot_mask != 0)
    114                     .map(|masked| masked.burn.clone())
    115                     .collect();
    116                 let burns_v2 = self
    117                     .burns_v2
    118                     .iter()
    119                     .filter(|masked| masked.bundle_mask & slot_mask != 0)
    120                     .map(|masked| masked.envelope.clone())
    121                     .collect();
    122                 BurnBundle {
    123                     height,
    124                     prev_hash: prev_hash.to_string(),
    125                     slot: signature.slot,
    126                     member: signature.member.clone(),
    127                     reward_address: signature.reward_address.clone(),
    128                     burns,
    129                     burns_v2,
    130                     signature: signature.signature.clone(),
    131                 }
    132             })
    133             .collect()
    134     }
    135 
    136     pub fn burn_bundle_hashes(
    137         &self,
    138         height: u64,
    139         prev_hash: &str,
    140         finalizer: &str,
    141     ) -> [String; BURN_COMMITTEE_SIZE] {
    142         let bundles = self.expand(height, prev_hash);
    143         let mut hashes = burn_bundle_hashes(&bundles);
    144         hashes[0] =
    145             finalizer_attestation_hash(height, prev_hash, finalizer, &self.burns, &self.burns_v2);
    146         hashes
    147     }
    148 
    149     pub(super) fn canonical(&self) -> String {
    150         let signatures = self
    151             .signatures
    152             .iter()
    153             .map(|signature| match &signature.reward_address {
    154                 Some(address) => format!(
    155                     "{}:{}:{}:{}",
    156                     signature.slot, signature.member, address, signature.signature
    157                 ),
    158                 None => format!(
    159                     "{}:{}:{}",
    160                     signature.slot, signature.member, signature.signature
    161                 ),
    162             })
    163             .collect::<Vec<_>>()
    164             .join("|");
    165         let burns = self
    166             .burns
    167             .iter()
    168             .map(|masked| format!("{}:{}", masked.bundle_mask, masked.burn.canonical()))
    169             .collect::<Vec<_>>()
    170             .join("|");
    171         let burns_v2 = self
    172             .burns_v2
    173             .iter()
    174             .map(|masked| format!("{}:{}", masked.bundle_mask, masked.envelope))
    175             .collect::<Vec<_>>()
    176             .join("|");
    177         if burns_v2.is_empty() {
    178             format!("burn-bundle-section-v1:{signatures}:burns:{burns}")
    179         } else {
    180             format!("burn-bundle-section-v2:{signatures}:burns:{burns}:burns-v2:{burns_v2}")
    181         }
    182     }
    183 }
    184 
    185 #[derive(Clone, Debug, Eq, PartialEq)]
    186 pub struct BurnBundlePayload {
    187     pub height: u64,
    188     pub prev_hash: String,
    189     pub slot: u8,
    190     pub member: String,
    191     pub reward_address: Option<String>,
    192     pub burns: Vec<Transaction>,
    193     pub burns_v2: Vec<String>,
    194 }
    195 
    196 impl BurnBundlePayload {
    197     pub fn canonical(&self) -> String {
    198         let burns = self
    199             .burns
    200             .iter()
    201             .map(Transaction::canonical)
    202             .collect::<Vec<_>>()
    203             .join("|");
    204         let burns_v2 = self.burns_v2.join("|");
    205         if !burns_v2.is_empty() {
    206             return format!(
    207                 "iuna-burn-bundle-v3:{}:{}:{}:{}:{}:{}:{}",
    208                 self.height,
    209                 self.prev_hash,
    210                 self.slot,
    211                 self.member,
    212                 self.reward_address.as_deref().unwrap_or(""),
    213                 burns,
    214                 burns_v2
    215             );
    216         }
    217         match &self.reward_address {
    218             Some(address) => format!(
    219                 "iuna-burn-bundle-v2:{}:{}:{}:{}:{}:{}",
    220                 self.height, self.prev_hash, self.slot, self.member, address, burns
    221             ),
    222             None => format!(
    223                 "iuna-burn-bundle-v1:{}:{}:{}:{}:{}",
    224                 self.height, self.prev_hash, self.slot, self.member, burns
    225             ),
    226         }
    227     }
    228 }
    229 
    230 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    231 #[serde(rename_all = "camelCase")]
    232 pub struct BurnCommitteeMember {
    233     pub slot: u8,
    234     pub root: String,
    235     pub owner: String,
    236     pub weight: Amount,
    237 }
    238 
    239 pub fn default_burn_bundle_hash(slot: usize) -> String {
    240     hex_hash(format!("iuna-default-burn-bundle-v1:{slot}"))
    241 }
    242 
    243 pub(super) fn burn_bundle_slot_mask(slot: u8) -> Result<u8> {
    244     if usize::from(slot) >= BURN_COMMITTEE_SIZE || slot >= 8 {
    245         bail!("burn bundle slot is invalid");
    246     }
    247     Ok(1_u8 << slot)
    248 }
    249 
    250 pub(super) fn burn_committee_mask() -> u8 {
    251     (0..BURN_COMMITTEE_SIZE).fold(0_u8, |mask, slot| mask | (1_u8 << slot))
    252 }
    253 
    254 pub(super) fn burn_bundle_hashes(bundles: &[BurnBundle]) -> [String; BURN_COMMITTEE_SIZE] {
    255     std::array::from_fn(|slot| {
    256         bundles
    257             .iter()
    258             .find(|bundle| usize::from(bundle.slot) == slot)
    259             .map(BurnBundle::bundle_hash)
    260             .unwrap_or_else(|| default_burn_bundle_hash(slot))
    261     })
    262 }
    263 
    264 pub(super) fn finalizer_attestation_hash(
    265     height: u64,
    266     prev_hash: &str,
    267     finalizer: &str,
    268     burns: &[MaskedBurn],
    269     burns_v2: &[MaskedBurnV2],
    270 ) -> String {
    271     let canonical_burns = burns
    272         .iter()
    273         .map(|masked| masked.burn.canonical())
    274         .collect::<Vec<_>>()
    275         .join("|");
    276     let canonical_burns_v2 = burns_v2
    277         .iter()
    278         .map(|masked| format!("{}:{}", masked.bundle_mask, masked.envelope))
    279         .collect::<Vec<_>>()
    280         .join("|");
    281     if canonical_burns_v2.is_empty() {
    282         hex_hash(format!(
    283             "iuna-finalizer-burn-attestation-v1:{height}:{prev_hash}:{finalizer}:{canonical_burns}"
    284         ))
    285     } else {
    286         hex_hash(format!(
    287             "iuna-finalizer-burn-attestation-v2:{height}:{prev_hash}:{finalizer}:{canonical_burns}:{canonical_burns_v2}"
    288         ))
    289     }
    290 }
    291 
    292 pub(super) fn canonical_burn_bundle_hashes(
    293     bundle_hashes: &[String; BURN_COMMITTEE_SIZE],
    294 ) -> String {
    295     bundle_hashes.join("|")
    296 }
    297 
    298 #[cfg(test)]
    299 mod tests {
    300     use super::*;
    301 
    302     #[test]
    303     fn burn_bundle_hash_array_covers_slots_zero_through_four() {
    304         let slot_four = BurnBundle {
    305             height: 1,
    306             prev_hash: "parent".to_string(),
    307             slot: 4,
    308             member: "member".to_string(),
    309             reward_address: None,
    310             burns: Vec::new(),
    311             burns_v2: Vec::new(),
    312             signature: "signature".to_string(),
    313         };
    314 
    315         let hashes = burn_bundle_hashes(std::slice::from_ref(&slot_four));
    316 
    317         assert_eq!(hashes.len(), 5);
    318         for (slot, hash) in hashes.iter().enumerate().take(4) {
    319             assert_eq!(hash, &default_burn_bundle_hash(slot));
    320         }
    321         assert_eq!(hashes[4], slot_four.bundle_hash());
    322     }
    323 }