invite_code.rs
1 use core::fmt; 2 use std::borrow::Cow; 3 use std::collections::BTreeMap; 4 use std::fmt::{Display, Formatter}; 5 use std::io::{Cursor, Read}; 6 use std::str::FromStr; 7 8 use anyhow::ensure; 9 use bech32::{Bech32m, Hrp}; 10 use serde::{Deserialize, Serialize}; 11 12 use crate::config::FederationId; 13 use crate::encoding::{Decodable, DecodeError, Encodable}; 14 use crate::module::registry::ModuleDecoderRegistry; 15 use crate::util::SafeUrl; 16 use crate::{NumPeersExt as _, PeerId}; 17 18 /// Information required for client to join Federation 19 /// 20 /// Can be used to download the configs and bootstrap a client. 21 /// 22 /// ## Invariants 23 /// Constructors have to guarantee that: 24 /// * At least one Api entry is present 25 /// * At least one Federation ID is present 26 #[derive(Clone, Debug, Eq, PartialEq, Encodable)] 27 pub struct InviteCode(Vec<InviteCodeData>); 28 29 impl Decodable for InviteCode { 30 fn consensus_decode<R: Read>( 31 r: &mut R, 32 modules: &ModuleDecoderRegistry, 33 ) -> Result<Self, DecodeError> { 34 let inner: Vec<InviteCodeData> = Decodable::consensus_decode(r, modules)?; 35 36 if !inner 37 .iter() 38 .any(|data| matches!(data, InviteCodeData::Api { .. })) 39 { 40 return Err(DecodeError::from_str( 41 "No API was provided in the invite code", 42 )); 43 } 44 45 if !inner 46 .iter() 47 .any(|data| matches!(data, InviteCodeData::FederationId(_))) 48 { 49 return Err(DecodeError::from_str( 50 "No Federation ID provided in invite code", 51 )); 52 } 53 54 Ok(InviteCode(inner)) 55 } 56 } 57 58 impl InviteCode { 59 pub fn new(url: SafeUrl, peer: PeerId, federation_id: FederationId) -> Self { 60 InviteCode(vec![ 61 InviteCodeData::Api { url, peer }, 62 InviteCodeData::FederationId(federation_id), 63 ]) 64 } 65 66 /// Constructs an [`InviteCode`] which contains as many guardian URLs as 67 /// needed to always be able to join a working federation 68 pub fn new_with_essential_num_guardians( 69 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>, 70 federation_id: FederationId, 71 ) -> Self { 72 let max_size = peer_to_url_map.max_evil() + 1; 73 let mut code_vec: Vec<InviteCodeData> = peer_to_url_map 74 .iter() 75 .take(max_size) 76 .map(|(peer, url)| InviteCodeData::Api { 77 url: url.clone(), 78 peer: *peer, 79 }) 80 .collect(); 81 code_vec.push(InviteCodeData::FederationId(federation_id)); 82 83 InviteCode(code_vec) 84 } 85 86 /// Returns the API URL of one of the guardians. 87 pub fn url(&self) -> SafeUrl { 88 self.0 89 .iter() 90 .find_map(|data| match data { 91 InviteCodeData::Api { url, .. } => Some(url.clone()), 92 _ => None, 93 }) 94 .expect("Ensured by constructor") 95 } 96 97 /// Returns the id of the guardian from which we got the API URL, see 98 /// [`InviteCode::url`]. 99 pub fn peer(&self) -> PeerId { 100 self.0 101 .iter() 102 .find_map(|data| match data { 103 InviteCodeData::Api { peer, .. } => Some(*peer), 104 _ => None, 105 }) 106 .expect("Ensured by constructor") 107 } 108 109 /// Get all peer URLs in the [`InviteCode`] 110 pub fn peers(&self) -> BTreeMap<PeerId, SafeUrl> { 111 self.0 112 .iter() 113 .filter_map(|entry| match entry { 114 InviteCodeData::Api { url, peer } => Some((*peer, url.clone())), 115 _ => None, 116 }) 117 .collect() 118 } 119 120 /// Returns the federation's ID that can be used to authenticate the config 121 /// downloaded from the API. 122 pub fn federation_id(&self) -> FederationId { 123 self.0 124 .iter() 125 .find_map(|data| match data { 126 InviteCodeData::FederationId(federation_id) => Some(*federation_id), 127 _ => None, 128 }) 129 .expect("Ensured by constructor") 130 } 131 } 132 133 /// Data that can be encoded in the invite code. Currently we always just use 134 /// one `Api` and one `FederationId` variant in an invite code, but more can be 135 /// added in the future while still keeping the invite code readable for older 136 /// clients, which will just ignore the new fields. 137 #[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)] 138 enum InviteCodeData { 139 /// API endpoint of one of the guardians 140 Api { 141 /// URL to reach an API that we can download configs from 142 url: SafeUrl, 143 /// Peer id of the host from the Url 144 peer: PeerId, 145 }, 146 /// Authentication id for the federation 147 FederationId(FederationId), 148 /// Unknown invite code fields to be defined in the future 149 #[encodable_default] 150 Default { variant: u64, bytes: Vec<u8> }, 151 } 152 153 /// We can represent client invite code as a bech32 string for compactness and 154 /// error-checking 155 /// 156 /// Human readable part (HRP) includes the version 157 /// ```txt 158 /// [ hrp (4 bytes) ] [ id (48 bytes) ] ([ url len (2 bytes) ] [ url bytes (url len bytes) ])+ 159 /// ``` 160 const BECH32_HRP: Hrp = Hrp::parse_unchecked("fed1"); 161 162 impl FromStr for InviteCode { 163 type Err = anyhow::Error; 164 165 fn from_str(encoded: &str) -> Result<Self, Self::Err> { 166 let (hrp, data) = bech32::decode(encoded)?; 167 168 ensure!(hrp == BECH32_HRP, "Invalid HRP in bech32 encoding"); 169 170 let invite = InviteCode::consensus_decode(&mut Cursor::new(data), &Default::default())?; 171 172 Ok(invite) 173 } 174 } 175 176 /// Parses the invite code from a bech32 string 177 impl Display for InviteCode { 178 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 179 let mut data = vec![]; 180 181 self.consensus_encode(&mut data) 182 .expect("Vec<u8> provides capacity"); 183 184 let encode = bech32::encode::<Bech32m>(BECH32_HRP, &data).map_err(|_| fmt::Error)?; 185 formatter.write_str(&encode) 186 } 187 } 188 189 impl Serialize for InviteCode { 190 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 191 where 192 S: serde::Serializer, 193 { 194 String::serialize(&self.to_string(), serializer) 195 } 196 } 197 198 impl<'de> Deserialize<'de> for InviteCode { 199 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 200 where 201 D: serde::Deserializer<'de>, 202 { 203 let string = Cow::<str>::deserialize(deserializer)?; 204 Self::from_str(&string).map_err(serde::de::Error::custom) 205 } 206 } 207 208 #[cfg(test)] 209 mod tests { 210 use std::str::FromStr; 211 212 use crate::config::FederationId; 213 use crate::invite_code::InviteCode; 214 215 #[test] 216 fn test_invite_code_to_from_string() { 217 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm"; 218 let invite_code = InviteCode::from_str(invite_code_str).expect("valid invite code"); 219 220 assert_eq!(invite_code.to_string(), invite_code_str); 221 assert_eq!( 222 invite_code.0, 223 [ 224 crate::invite_code::InviteCodeData::Api { 225 url: "wss://fedimintd.mplsfed.foo/".parse().expect("valid url"), 226 peer: crate::PeerId(0), 227 }, 228 crate::invite_code::InviteCodeData::FederationId(FederationId( 229 bitcoin_hashes::sha256::Hash::from_str( 230 "bea7ff4116f2b1d324c7b5d699cce4ac7408cee41db2c88027e21b76fff3b9f4" 231 ) 232 .expect("valid hash") 233 )) 234 ] 235 ); 236 } 237 }