Skip to main content

fedimint_core/net/
guardian_metadata.rs

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";
9/// Allow messages with timestamps up to 1 hour in the future
10const 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    /// z-base32 encoded Pkarr id
16    pub pkarr_id_z32: String,
17    pub timestamp_secs: u64,
18    /// Iroh-next 1.0-compatible API endpoint node ID (public key) when enabled
19    #[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    /// The raw bytes that were signed (JSON-encoded GuardianMetadata)
26    pub bytes: Vec<u8>,
27    /// The parsed GuardianMetadata value
28    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
39// Implement Serialize/Deserialize for SignedGuardianMetadata for JSON
40//
41// Format: {"content": "<json string>", "signature": "<hex-encoded signature>"}
42// The `content` field contains the exact JSON string that was signed (preserved
43// byte-for-byte). The `signature` field contains the hex-encoded Schnorr
44// signature over the content bytes.
45impl 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        // Serialize bytes as a UTF-8 string (content field)
54        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        // Serialize signature as hex string
59        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
93// Implement Encodable/Decodable for SignedGuardianMetadata only
94impl Encodable for SignedGuardianMetadata {
95    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
96        // Encode the bytes and signature (value is derived from bytes)
97        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    /// Set the iroh-next 1.0-compatible API endpoint node ID.
153    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        // Serialize to JSON and compute tagged hash
164        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    /// Returns the parsed GuardianMetadata value
180    pub fn guardian_metadata(&self) -> &GuardianMetadata {
181        &self.value
182    }
183
184    /// Compute the tagged hash from the stored bytes
185    pub fn tagged_hash(&self) -> sha256::Hash {
186        compute_tagged_hash(&self.bytes)
187    }
188
189    /// Verifies the signature and timestamp validity.
190    ///
191    /// Returns `Ok(())` if the signature is valid for the given public key and
192    /// the timestamp is not too far in the future relative to `now`.
193    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        // First check the signature
200        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        // Then check the timestamp isn't too far in the future
205        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        // Serialize to JSON
242        let json = serde_json::to_string(&signed).expect("serialization should succeed");
243
244        // Verify JSON structure
245        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        // Deserialize from JSON
256        let deserialized: SignedGuardianMetadata =
257            serde_json::from_str(&json).expect("deserialization should succeed");
258
259        // Compare original and deserialized
260        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        // Verify signature still works after roundtrip
266        let now = Duration::from_secs(timestamp_secs);
267        deserialized
268            .verify(&ctx, &public_key, now)
269            .expect("signature should verify after roundtrip");
270
271        // Verify extracted metadata matches original
272        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        // Encode to bytes
291        let encoded = signed.consensus_encode_to_vec();
292
293        // Decode from bytes
294        let deserialized: SignedGuardianMetadata =
295            Decodable::consensus_decode_whole(&encoded, &ModuleRegistry::default())
296                .expect("decoding should succeed");
297
298        // Compare original and deserialized
299        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        // Verify signature still works after roundtrip
305        let now = Duration::from_secs(timestamp_secs);
306        deserialized
307            .verify(&ctx, &public_key, now)
308            .expect("signature should verify after roundtrip");
309
310        // Verify extracted metadata matches original
311        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        // Verify succeeds when now == timestamp
329        signed
330            .verify(&ctx, &public_key, Duration::from_secs(timestamp_secs))
331            .expect("should verify with matching timestamp");
332
333        // Verify succeeds when now is after timestamp (metadata from the past)
334        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        // Verify succeeds when timestamp is slightly in the future (within allowed
343        // window)
344        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        // Verify fails with wrong public key
369        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        // Verify fails when timestamp is too far in the future
391        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}