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