1use bitcoin::hashes::{Hash, sha256};
2use bitcoin::secp256k1::Message;
3use fedimint_core::encoding::{Decodable, DecodeContext as _, 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(DecodeError::from_err)
111 .context("Invalid JSON")?;
112 let signature = secp256k1::schnorr::Signature::consensus_decode_partial_from_finite_reader(
113 reader, modules,
114 )?;
115
116 Ok(Self {
117 bytes,
118 value,
119 signature,
120 })
121 }
122}
123
124fn compute_tagged_hash(json_bytes: &[u8]) -> sha256::Hash {
125 use bitcoin::hashes::HashEngine;
126 let mut engine = sha256::HashEngine::default();
127 engine.input(GUARDIAN_METADATA_MESSAGE_TAG);
128 engine.input(json_bytes);
129 sha256::Hash::from_engine(engine)
130}
131
132#[derive(Debug, thiserror::Error)]
133pub enum VerificationError {
134 #[error("Invalid signature")]
135 InvalidSignature,
136 #[error("Timestamp {timestamp_secs} is too far in the future (max allowed: {max_allowed})")]
137 TimestampTooFarInFuture {
138 timestamp_secs: u64,
139 max_allowed: u64,
140 },
141}
142
143impl GuardianMetadata {
144 pub fn new(api_urls: Vec<SafeUrl>, pkarr_id_z32: String, timestamp_secs: u64) -> Self {
145 Self {
146 api_urls,
147 pkarr_id_z32,
148 timestamp_secs,
149 iroh_next_endpoint: None,
150 }
151 }
152
153 pub fn with_iroh_next_endpoint(mut self, endpoint: String) -> Self {
155 self.iroh_next_endpoint = Some(endpoint);
156 self
157 }
158
159 pub fn sign<C: secp256k1::Signing>(
160 &self,
161 ctx: &secp256k1::Secp256k1<C>,
162 key: &secp256k1::Keypair,
163 ) -> SignedGuardianMetadata {
164 let bytes = serde_json::to_vec(self).expect("JSON serialization should not fail");
166 let tagged_hash = compute_tagged_hash(&bytes);
167
168 let msg = Message::from_digest(*tagged_hash.as_ref());
169 let signature = ctx.sign_schnorr(&msg, key);
170
171 SignedGuardianMetadata {
172 bytes,
173 value: self.clone(),
174 signature,
175 }
176 }
177}
178
179impl SignedGuardianMetadata {
180 pub fn guardian_metadata(&self) -> &GuardianMetadata {
182 &self.value
183 }
184
185 pub fn tagged_hash(&self) -> sha256::Hash {
187 compute_tagged_hash(&self.bytes)
188 }
189
190 pub fn verify<C: secp256k1::Verification>(
195 &self,
196 ctx: &secp256k1::Secp256k1<C>,
197 pk: &secp256k1::PublicKey,
198 now: std::time::Duration,
199 ) -> Result<(), VerificationError> {
200 let msg = Message::from_digest(*self.tagged_hash().as_ref());
202 ctx.verify_schnorr(&self.signature, &msg, &pk.x_only_public_key().0)
203 .map_err(|_| VerificationError::InvalidSignature)?;
204
205 let current_secs = now.as_secs();
207 let max_allowed_timestamp = current_secs.saturating_add(MAX_FUTURE_TIMESTAMP_SECS);
208
209 if max_allowed_timestamp < self.value.timestamp_secs {
210 return Err(VerificationError::TimestampTooFarInFuture {
211 timestamp_secs: self.value.timestamp_secs,
212 max_allowed: max_allowed_timestamp,
213 });
214 }
215
216 Ok(())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use std::time::Duration;
223
224 use super::*;
225 use crate::module::registry::ModuleRegistry;
226
227 #[test]
228 fn signed_guardian_metadata_json_roundtrip() {
229 let ctx = secp256k1::Secp256k1::new();
230 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
231 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
232
233 let timestamp_secs = 1000;
234 let metadata = GuardianMetadata::new(
235 vec!["wss://example.com/api".parse().unwrap()],
236 "test_pkarr_id".to_string(),
237 timestamp_secs,
238 );
239
240 let signed = metadata.sign(&ctx, &keypair);
241
242 let json = serde_json::to_string(&signed).expect("serialization should succeed");
244
245 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
247 assert!(
248 json_value.get("content").is_some(),
249 "should have content field"
250 );
251 assert!(
252 json_value.get("signature").is_some(),
253 "should have signature field"
254 );
255
256 let deserialized: SignedGuardianMetadata =
258 serde_json::from_str(&json).expect("deserialization should succeed");
259
260 assert_eq!(signed.bytes, deserialized.bytes);
262 assert_eq!(signed.value, deserialized.value);
263 assert_eq!(signed.signature, deserialized.signature);
264 assert_eq!(signed, deserialized);
265
266 let now = Duration::from_secs(timestamp_secs);
268 deserialized
269 .verify(&ctx, &public_key, now)
270 .expect("signature should verify after roundtrip");
271
272 assert_eq!(*deserialized.guardian_metadata(), metadata);
274 }
275
276 #[test]
277 fn signed_guardian_metadata_encodable_roundtrip() {
278 let ctx = secp256k1::Secp256k1::new();
279 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
280 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
281
282 let timestamp_secs = 1000;
283 let metadata = GuardianMetadata::new(
284 vec!["wss://example.com/api".parse().unwrap()],
285 "test_pkarr_id".to_string(),
286 timestamp_secs,
287 );
288
289 let signed = metadata.sign(&ctx, &keypair);
290
291 let encoded = signed.consensus_encode_to_vec();
293
294 let deserialized: SignedGuardianMetadata =
296 Decodable::consensus_decode_whole(&encoded, &ModuleRegistry::default())
297 .expect("decoding should succeed");
298
299 assert_eq!(signed.bytes, deserialized.bytes);
301 assert_eq!(signed.value, deserialized.value);
302 assert_eq!(signed.signature, deserialized.signature);
303 assert_eq!(signed, deserialized);
304
305 let now = Duration::from_secs(timestamp_secs);
307 deserialized
308 .verify(&ctx, &public_key, now)
309 .expect("signature should verify after roundtrip");
310
311 assert_eq!(*deserialized.guardian_metadata(), metadata);
313 }
314
315 #[test]
316 fn verify_valid_signature_and_timestamp() {
317 let ctx = secp256k1::Secp256k1::new();
318 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
319 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
320
321 let timestamp_secs = 10000;
322 let metadata = GuardianMetadata::new(
323 vec!["wss://example.com/api".parse().unwrap()],
324 "test_pkarr_id".to_string(),
325 timestamp_secs,
326 );
327 let signed = metadata.sign(&ctx, &keypair);
328
329 signed
331 .verify(&ctx, &public_key, Duration::from_secs(timestamp_secs))
332 .expect("should verify with matching timestamp");
333
334 signed
336 .verify(
337 &ctx,
338 &public_key,
339 Duration::from_secs(timestamp_secs + 1000),
340 )
341 .expect("should verify with past timestamp");
342
343 signed
346 .verify(
347 &ctx,
348 &public_key,
349 Duration::from_secs(timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS),
350 )
351 .expect("should verify when timestamp is within allowed future window");
352 }
353
354 #[test]
355 fn verify_rejects_invalid_signature() {
356 let ctx = secp256k1::Secp256k1::new();
357 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
358 let wrong_keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
359 let wrong_public_key = secp256k1::PublicKey::from_keypair(&wrong_keypair);
360
361 let timestamp_secs = 1000;
362 let metadata = GuardianMetadata::new(
363 vec!["wss://example.com/api".parse().unwrap()],
364 "test_pkarr_id".to_string(),
365 timestamp_secs,
366 );
367 let signed = metadata.sign(&ctx, &keypair);
368
369 let result = signed.verify(&ctx, &wrong_public_key, Duration::from_secs(timestamp_secs));
371 assert!(
372 matches!(result, Err(VerificationError::InvalidSignature)),
373 "should reject invalid signature"
374 );
375 }
376
377 #[test]
378 fn verify_rejects_timestamp_too_far_in_future() {
379 let ctx = secp256k1::Secp256k1::new();
380 let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
381 let public_key = secp256k1::PublicKey::from_keypair(&keypair);
382
383 let timestamp_secs = 10000;
384 let metadata = GuardianMetadata::new(
385 vec!["wss://example.com/api".parse().unwrap()],
386 "test_pkarr_id".to_string(),
387 timestamp_secs,
388 );
389 let signed = metadata.sign(&ctx, &keypair);
390
391 let now_secs = timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS - 1;
393 let result = signed.verify(&ctx, &public_key, Duration::from_secs(now_secs));
394 assert!(
395 matches!(
396 result,
397 Err(VerificationError::TimestampTooFarInFuture {
398 timestamp_secs: ts,
399 ..
400 }) if ts == timestamp_secs
401 ),
402 "should reject timestamp too far in future"
403 );
404 }
405}