1use bitcoin::hashes::{Hash, sha256};
2use bitcoin::secp256k1::Message;
3use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
4use serde::{Deserialize, Serialize};
5
6use crate::util::SafeUrl;
7
8const GUARDIAN_METADATA_MESSAGE_TAG: &[u8] = b"fedimint-guardian-metadata";
9const MAX_FUTURE_TIMESTAMP_SECS: u64 = 3600;
11
12#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)]
13pub struct GuardianMetadata {
14 pub api_urls: Vec<SafeUrl>,
15 pub pkarr_id_z32: String,
17 pub timestamp_secs: u64,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub iroh_next_endpoint: Option<String>,
21}
22
23#[derive(Debug, Clone, Eq, Hash, PartialEq)]
24pub struct SignedGuardianMetadata {
25 pub bytes: Vec<u8>,
27 pub value: GuardianMetadata,
29 pub signature: secp256k1::schnorr::Signature,
30}
31
32#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Encodable, Decodable)]
33pub struct SignedGuardianMetadataSubmission {
34 #[serde(flatten)]
35 pub signed_guardian_metadata: SignedGuardianMetadata,
36 pub peer_id: crate::PeerId,
37}
38
39impl Serialize for SignedGuardianMetadata {
46 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
47 where
48 S: serde::Serializer,
49 {
50 use serde::ser::SerializeStruct;
51 let mut state = serializer.serialize_struct("SignedGuardianMetadata", 2)?;
52
53 let content = String::from_utf8(self.bytes.clone())
55 .map_err(|e| serde::ser::Error::custom(format!("Invalid UTF-8 in bytes: {e}")))?;
56 state.serialize_field("content", &content)?;
57
58 state.serialize_field("signature", &hex::encode(self.signature.as_ref()))?;
60 state.end()
61 }
62}
63
64impl<'de> Deserialize<'de> for SignedGuardianMetadata {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 use serde::de::Error;
70
71 #[derive(Deserialize)]
72 struct SignedGuardianMetadataHelper {
73 content: String,
74 signature: String,
75 }
76
77 let helper = SignedGuardianMetadataHelper::deserialize(deserializer)?;
78
79 let bytes = helper.content.into_bytes();
80 let value: GuardianMetadata = serde_json::from_slice(&bytes).map_err(D::Error::custom)?;
81 let signature_bytes = hex::decode(&helper.signature).map_err(D::Error::custom)?;
82 let signature = secp256k1::schnorr::Signature::from_slice(&signature_bytes)
83 .map_err(D::Error::custom)?;
84
85 Ok(Self {
86 bytes,
87 value,
88 signature,
89 })
90 }
91}
92
93impl Encodable for SignedGuardianMetadata {
95 fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
96 self.bytes.consensus_encode(writer)?;
98 self.signature.consensus_encode(writer)?;
99 Ok(())
100 }
101}
102
103impl Decodable for SignedGuardianMetadata {
104 fn consensus_decode_partial_from_finite_reader<R: std::io::Read>(
105 reader: &mut R,
106 modules: &fedimint_core::module::registry::ModuleDecoderRegistry,
107 ) -> Result<Self, DecodeError> {
108 let bytes = Vec::<u8>::consensus_decode_partial_from_finite_reader(reader, modules)?;
109 let value: GuardianMetadata = serde_json::from_slice(&bytes)
110 .map_err(|e| DecodeError::new_custom(anyhow::anyhow!("Invalid JSON: {e}")))?;
111 let signature = secp256k1::schnorr::Signature::consensus_decode_partial_from_finite_reader(
112 reader, modules,
113 )?;
114
115 Ok(Self {
116 bytes,
117 value,
118 signature,
119 })
120 }
121}
122
123fn compute_tagged_hash(json_bytes: &[u8]) -> sha256::Hash {
124 use bitcoin::hashes::HashEngine;
125 let mut engine = sha256::HashEngine::default();
126 engine.input(GUARDIAN_METADATA_MESSAGE_TAG);
127 engine.input(json_bytes);
128 sha256::Hash::from_engine(engine)
129}
130
131#[derive(Debug, thiserror::Error)]
132pub enum VerificationError {
133 #[error("Invalid signature")]
134 InvalidSignature,
135 #[error("Timestamp {timestamp_secs} is too far in the future (max allowed: {max_allowed})")]
136 TimestampTooFarInFuture {
137 timestamp_secs: u64,
138 max_allowed: u64,
139 },
140}
141
142impl GuardianMetadata {
143 pub fn new(api_urls: Vec<SafeUrl>, pkarr_id_z32: String, timestamp_secs: u64) -> Self {
144 Self {
145 api_urls,
146 pkarr_id_z32,
147 timestamp_secs,
148 iroh_next_endpoint: None,
149 }
150 }
151
152 pub fn with_iroh_next_endpoint(mut self, endpoint: String) -> Self {
154 self.iroh_next_endpoint = Some(endpoint);
155 self
156 }
157
158 pub fn sign<C: secp256k1::Signing>(
159 &self,
160 ctx: &secp256k1::Secp256k1<C>,
161 key: &secp256k1::Keypair,
162 ) -> SignedGuardianMetadata {
163 let bytes = serde_json::to_vec(self).expect("JSON serialization should not fail");
165 let tagged_hash = compute_tagged_hash(&bytes);
166
167 let msg = Message::from_digest(*tagged_hash.as_ref());
168 let signature = ctx.sign_schnorr(&msg, key);
169
170 SignedGuardianMetadata {
171 bytes,
172 value: self.clone(),
173 signature,
174 }
175 }
176}
177
178impl SignedGuardianMetadata {
179 pub fn guardian_metadata(&self) -> &GuardianMetadata {
181 &self.value
182 }
183
184 pub fn tagged_hash(&self) -> sha256::Hash {
186 compute_tagged_hash(&self.bytes)
187 }
188
189 pub fn verify<C: secp256k1::Verification>(
194 &self,
195 ctx: &secp256k1::Secp256k1<C>,
196 pk: &secp256k1::PublicKey,
197 now: std::time::Duration,
198 ) -> Result<(), VerificationError> {
199 let msg = Message::from_digest(*self.tagged_hash().as_ref());
201 ctx.verify_schnorr(&self.signature, &msg, &pk.x_only_public_key().0)
202 .map_err(|_| VerificationError::InvalidSignature)?;
203
204 let current_secs = now.as_secs();
206 let max_allowed_timestamp = current_secs.saturating_add(MAX_FUTURE_TIMESTAMP_SECS);
207
208 if max_allowed_timestamp < self.value.timestamp_secs {
209 return Err(VerificationError::TimestampTooFarInFuture {
210 timestamp_secs: self.value.timestamp_secs,
211 max_allowed: max_allowed_timestamp,
212 });
213 }
214
215 Ok(())
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use std::time::Duration;
222
223 use super::*;
224 use crate::module::registry::ModuleRegistry;
225
226 #[test]
227 fn signed_guardian_metadata_json_roundtrip() {
228 let ctx = secp256k1::Secp256k1::new();
229 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
230 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
231
232 let timestamp_secs = 1000;
233 let metadata = GuardianMetadata::new(
234 vec!["wss://example.com/api".parse().unwrap()],
235 "test_pkarr_id".to_string(),
236 timestamp_secs,
237 );
238
239 let signed = metadata.sign(&ctx, &keypair);
240
241 let json = serde_json::to_string(&signed).expect("serialization should succeed");
243
244 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
246 assert!(
247 json_value.get("content").is_some(),
248 "should have content field"
249 );
250 assert!(
251 json_value.get("signature").is_some(),
252 "should have signature field"
253 );
254
255 let deserialized: SignedGuardianMetadata =
257 serde_json::from_str(&json).expect("deserialization should succeed");
258
259 assert_eq!(signed.bytes, deserialized.bytes);
261 assert_eq!(signed.value, deserialized.value);
262 assert_eq!(signed.signature, deserialized.signature);
263 assert_eq!(signed, deserialized);
264
265 let now = Duration::from_secs(timestamp_secs);
267 deserialized
268 .verify(&ctx, &public_key, now)
269 .expect("signature should verify after roundtrip");
270
271 assert_eq!(*deserialized.guardian_metadata(), metadata);
273 }
274
275 #[test]
276 fn signed_guardian_metadata_encodable_roundtrip() {
277 let ctx = secp256k1::Secp256k1::new();
278 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
279 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
280
281 let timestamp_secs = 1000;
282 let metadata = GuardianMetadata::new(
283 vec!["wss://example.com/api".parse().unwrap()],
284 "test_pkarr_id".to_string(),
285 timestamp_secs,
286 );
287
288 let signed = metadata.sign(&ctx, &keypair);
289
290 let encoded = signed.consensus_encode_to_vec();
292
293 let deserialized: SignedGuardianMetadata =
295 Decodable::consensus_decode_whole(&encoded, &ModuleRegistry::default())
296 .expect("decoding should succeed");
297
298 assert_eq!(signed.bytes, deserialized.bytes);
300 assert_eq!(signed.value, deserialized.value);
301 assert_eq!(signed.signature, deserialized.signature);
302 assert_eq!(signed, deserialized);
303
304 let now = Duration::from_secs(timestamp_secs);
306 deserialized
307 .verify(&ctx, &public_key, now)
308 .expect("signature should verify after roundtrip");
309
310 assert_eq!(*deserialized.guardian_metadata(), metadata);
312 }
313
314 #[test]
315 fn verify_valid_signature_and_timestamp() {
316 let ctx = secp256k1::Secp256k1::new();
317 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
318 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
319
320 let timestamp_secs = 10000;
321 let metadata = GuardianMetadata::new(
322 vec!["wss://example.com/api".parse().unwrap()],
323 "test_pkarr_id".to_string(),
324 timestamp_secs,
325 );
326 let signed = metadata.sign(&ctx, &keypair);
327
328 signed
330 .verify(&ctx, &public_key, Duration::from_secs(timestamp_secs))
331 .expect("should verify with matching timestamp");
332
333 signed
335 .verify(
336 &ctx,
337 &public_key,
338 Duration::from_secs(timestamp_secs + 1000),
339 )
340 .expect("should verify with past timestamp");
341
342 signed
345 .verify(
346 &ctx,
347 &public_key,
348 Duration::from_secs(timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS),
349 )
350 .expect("should verify when timestamp is within allowed future window");
351 }
352
353 #[test]
354 fn verify_rejects_invalid_signature() {
355 let ctx = secp256k1::Secp256k1::new();
356 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
357 let wrong_keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
358 let wrong_public_key = secp256k1::PublicKey::from_keypair(&wrong_keypair);
359
360 let timestamp_secs = 1000;
361 let metadata = GuardianMetadata::new(
362 vec!["wss://example.com/api".parse().unwrap()],
363 "test_pkarr_id".to_string(),
364 timestamp_secs,
365 );
366 let signed = metadata.sign(&ctx, &keypair);
367
368 let result = signed.verify(&ctx, &wrong_public_key, Duration::from_secs(timestamp_secs));
370 assert!(
371 matches!(result, Err(VerificationError::InvalidSignature)),
372 "should reject invalid signature"
373 );
374 }
375
376 #[test]
377 fn verify_rejects_timestamp_too_far_in_future() {
378 let ctx = secp256k1::Secp256k1::new();
379 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
380 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
381
382 let timestamp_secs = 10000;
383 let metadata = GuardianMetadata::new(
384 vec!["wss://example.com/api".parse().unwrap()],
385 "test_pkarr_id".to_string(),
386 timestamp_secs,
387 );
388 let signed = metadata.sign(&ctx, &keypair);
389
390 let now_secs = timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS - 1;
392 let result = signed.verify(&ctx, &public_key, Duration::from_secs(now_secs));
393 assert!(
394 matches!(
395 result,
396 Err(VerificationError::TimestampTooFarInFuture {
397 timestamp_secs: ts,
398 ..
399 }) if ts == timestamp_secs
400 ),
401 "should reject timestamp too far in future"
402 );
403 }
404}