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 }