reveal.rs (8299B)
1 use anyhow::{Context, Result, bail}; 2 use serde::{Deserialize, Serialize}; 3 4 use super::{Amount, BlindedReveal, REVEAL_COMMITTEE_SIZE, hex_hash}; 5 6 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 7 #[serde(rename_all = "camelCase")] 8 pub struct RevealBundle { 9 pub height: u64, 10 pub prev_hash: String, 11 pub slot: u8, 12 pub member: String, 13 pub reveals: Vec<BlindedReveal>, 14 pub signature: String, 15 } 16 17 impl RevealBundle { 18 pub fn canonical_payload(&self) -> String { 19 RevealBundlePayload { 20 height: self.height, 21 prev_hash: self.prev_hash.clone(), 22 slot: self.slot, 23 member: self.member.clone(), 24 reveals: self.reveals.clone(), 25 } 26 .canonical() 27 } 28 29 pub fn canonical(&self) -> String { 30 format!("{}:{}", self.canonical_payload(), self.signature) 31 } 32 33 pub fn bundle_hash(&self) -> String { 34 hex_hash(self.canonical()) 35 } 36 37 pub fn serialized_size_bytes(&self) -> Result<usize> { 38 serde_json::to_vec(self) 39 .map(|bytes| bytes.len()) 40 .context("failed to serialize reveal bundle for size check") 41 } 42 } 43 44 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 45 #[serde(rename_all = "camelCase")] 46 pub struct RevealBundleSignature { 47 pub slot: u8, 48 pub member: String, 49 pub signature: String, 50 } 51 52 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 53 #[serde(rename_all = "camelCase")] 54 pub struct MaskedBlindedReveal { 55 pub reveal: BlindedReveal, 56 pub bundle_mask: u8, 57 } 58 59 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 60 #[serde(rename_all = "camelCase")] 61 pub struct RevealBundleSection { 62 #[serde(default, skip_serializing_if = "Vec::is_empty")] 63 pub signatures: Vec<RevealBundleSignature>, 64 #[serde(default, skip_serializing_if = "Vec::is_empty")] 65 pub reveals: Vec<MaskedBlindedReveal>, 66 } 67 68 impl RevealBundleSection { 69 pub fn is_empty(&self) -> bool { 70 self.signatures.is_empty() && self.reveals.is_empty() 71 } 72 73 pub fn all_reveals(&self) -> Vec<&BlindedReveal> { 74 self.reveals.iter().map(|masked| &masked.reveal).collect() 75 } 76 77 pub fn included_bundle_count(&self) -> usize { 78 self.signatures.len() 79 } 80 81 pub fn expand(&self, height: u64, prev_hash: &str) -> Vec<RevealBundle> { 82 self.signatures 83 .iter() 84 .map(|signature| { 85 let slot_mask = reveal_bundle_slot_mask(signature.slot).unwrap_or(0); 86 let reveals = self 87 .reveals 88 .iter() 89 .filter(|masked| masked.bundle_mask & slot_mask != 0) 90 .map(|masked| masked.reveal.clone()) 91 .collect(); 92 RevealBundle { 93 height, 94 prev_hash: prev_hash.to_string(), 95 slot: signature.slot, 96 member: signature.member.clone(), 97 reveals, 98 signature: signature.signature.clone(), 99 } 100 }) 101 .collect() 102 } 103 104 pub fn reveal_bundle_hashes( 105 &self, 106 height: u64, 107 prev_hash: &str, 108 ) -> [String; REVEAL_COMMITTEE_SIZE] { 109 let bundles = self.expand(height, prev_hash); 110 reveal_bundle_hashes(&bundles) 111 } 112 113 pub(super) fn canonical(&self) -> String { 114 let signatures = self 115 .signatures 116 .iter() 117 .map(|signature| { 118 format!( 119 "{}:{}:{}", 120 signature.slot, signature.member, signature.signature 121 ) 122 }) 123 .collect::<Vec<_>>() 124 .join("|"); 125 let reveals = self 126 .reveals 127 .iter() 128 .map(|masked| format!("{}:{}", masked.bundle_mask, masked.reveal.canonical())) 129 .collect::<Vec<_>>() 130 .join("|"); 131 format!("reveal-bundle-section-v1:{signatures}:reveals:{reveals}") 132 } 133 } 134 135 #[derive(Clone, Debug, Eq, PartialEq)] 136 pub(super) struct RevealBundlePayload { 137 pub(super) height: u64, 138 pub(super) prev_hash: String, 139 pub(super) slot: u8, 140 pub(super) member: String, 141 pub(super) reveals: Vec<BlindedReveal>, 142 } 143 144 impl RevealBundlePayload { 145 pub(super) fn canonical(&self) -> String { 146 let reveals = self 147 .reveals 148 .iter() 149 .map(BlindedReveal::canonical) 150 .collect::<Vec<_>>() 151 .join("|"); 152 format!( 153 "iuna-reveal-bundle-v1:{}:{}:{}:{}:{}", 154 self.height, self.prev_hash, self.slot, self.member, reveals 155 ) 156 } 157 } 158 159 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 160 #[serde(rename_all = "camelCase")] 161 pub struct RevealCommitteeMember { 162 pub slot: u8, 163 pub rank: u32, 164 pub ticket_id: String, 165 pub owner: String, 166 pub amount: Amount, 167 } 168 169 pub fn default_reveal_bundle_hash(slot: usize) -> String { 170 hex_hash(format!("iuna-default-reveal-bundle-v1:{slot}")) 171 } 172 173 pub(super) fn reveal_bundle_slot_mask(slot: u8) -> Result<u8> { 174 if usize::from(slot) >= REVEAL_COMMITTEE_SIZE || slot >= 8 { 175 bail!("reveal bundle slot is invalid"); 176 } 177 Ok(1_u8 << slot) 178 } 179 180 pub(super) fn reveal_committee_mask() -> u8 { 181 (0..REVEAL_COMMITTEE_SIZE).fold(0_u8, |mask, slot| mask | (1_u8 << slot)) 182 } 183 184 pub(super) fn reveal_bundle_hashes(bundles: &[RevealBundle]) -> [String; REVEAL_COMMITTEE_SIZE] { 185 std::array::from_fn(|slot| { 186 bundles 187 .iter() 188 .find(|bundle| usize::from(bundle.slot) == slot) 189 .map(RevealBundle::bundle_hash) 190 .unwrap_or_else(|| default_reveal_bundle_hash(slot)) 191 }) 192 } 193 194 pub(super) fn canonical_reveal_bundle_hashes( 195 bundle_hashes: &[String; REVEAL_COMMITTEE_SIZE], 196 ) -> String { 197 bundle_hashes.join("|") 198 } 199 200 #[cfg(test)] 201 mod tests { 202 use super::{ 203 MaskedBlindedReveal, RevealBundle, RevealBundlePayload, RevealBundleSection, 204 RevealBundleSignature, default_reveal_bundle_hash, reveal_bundle_hashes, 205 reveal_bundle_slot_mask, 206 }; 207 use crate::domain::BlindedReveal; 208 209 #[test] 210 fn reveal_bundle_payload_is_canonical() { 211 let payload = RevealBundlePayload { 212 height: 7, 213 prev_hash: "prev".to_string(), 214 slot: 1, 215 member: "member".to_string(), 216 reveals: vec![BlindedReveal { 217 commitment: "commitment".to_string(), 218 key: "key".to_string(), 219 }], 220 }; 221 222 assert_eq!( 223 payload.canonical(), 224 "iuna-reveal-bundle-v1:7:prev:1:member:blinded-reveal:commitment:key" 225 ); 226 } 227 228 #[test] 229 fn reveal_bundle_hashes_fill_missing_slots_with_defaults() { 230 let bundle = RevealBundle { 231 height: 1, 232 prev_hash: "prev".to_string(), 233 slot: 1, 234 member: "member".to_string(), 235 reveals: Vec::new(), 236 signature: "sig".to_string(), 237 }; 238 239 let hashes = reveal_bundle_hashes(&[bundle.clone()]); 240 241 assert_eq!(hashes[0], default_reveal_bundle_hash(0)); 242 assert_eq!(hashes[1], bundle.bundle_hash()); 243 assert_eq!(hashes[2], default_reveal_bundle_hash(2)); 244 } 245 246 #[test] 247 fn reveal_bundle_section_expands_masked_reveals_by_slot() { 248 let reveal = BlindedReveal { 249 commitment: "commitment".to_string(), 250 key: "key".to_string(), 251 }; 252 let section = RevealBundleSection { 253 signatures: vec![ 254 RevealBundleSignature { 255 slot: 0, 256 member: "a".to_string(), 257 signature: "sig-a".to_string(), 258 }, 259 RevealBundleSignature { 260 slot: 1, 261 member: "b".to_string(), 262 signature: "sig-b".to_string(), 263 }, 264 ], 265 reveals: vec![MaskedBlindedReveal { 266 reveal: reveal.clone(), 267 bundle_mask: reveal_bundle_slot_mask(1).unwrap(), 268 }], 269 }; 270 271 let expanded = section.expand(3, "prev"); 272 273 assert!(expanded[0].reveals.is_empty()); 274 assert_eq!(expanded[1].reveals, vec![reveal]); 275 } 276 }