use bitcoin::hashes::{Hash, sha256};
use bitcoin::secp256k1::Message;
use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
use serde::{Deserialize, Serialize};
use crate::util::SafeUrl;
const GUARDIAN_METADATA_MESSAGE_TAG: &[u8] = b"fedimint-guardian-metadata";
const MAX_FUTURE_TIMESTAMP_SECS: u64 = 3600;
#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)]
pub struct GuardianMetadata {
pub api_urls: Vec<SafeUrl>,
pub pkarr_id_z32: String,
pub timestamp_secs: u64,
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct SignedGuardianMetadata {
pub bytes: Vec<u8>,
pub value: GuardianMetadata,
pub signature: secp256k1::schnorr::Signature,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Encodable, Decodable)]
pub struct SignedGuardianMetadataSubmission {
#[serde(flatten)]
pub signed_guardian_metadata: SignedGuardianMetadata,
pub peer_id: crate::PeerId,
}
impl Serialize for SignedGuardianMetadata {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("SignedGuardianMetadata", 2)?;
let content = String::from_utf8(self.bytes.clone())
.map_err(|e| serde::ser::Error::custom(format!("Invalid UTF-8 in bytes: {e}")))?;
state.serialize_field("content", &content)?;
state.serialize_field("signature", &hex::encode(self.signature.as_ref()))?;
state.end()
}
}
impl<'de> Deserialize<'de> for SignedGuardianMetadata {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
struct SignedGuardianMetadataHelper {
content: String,
signature: String,
}
let helper = SignedGuardianMetadataHelper::deserialize(deserializer)?;
let bytes = helper.content.into_bytes();
let value: GuardianMetadata = serde_json::from_slice(&bytes).map_err(D::Error::custom)?;
let signature_bytes = hex::decode(&helper.signature).map_err(D::Error::custom)?;
let signature = secp256k1::schnorr::Signature::from_slice(&signature_bytes)
.map_err(D::Error::custom)?;
Ok(Self {
bytes,
value,
signature,
})
}
}
impl Encodable for SignedGuardianMetadata {
fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
self.bytes.consensus_encode(writer)?;
self.signature.consensus_encode(writer)?;
Ok(())
}
}
impl Decodable for SignedGuardianMetadata {
fn consensus_decode_partial_from_finite_reader<R: std::io::Read>(
reader: &mut R,
modules: &fedimint_core::module::registry::ModuleDecoderRegistry,
) -> Result<Self, DecodeError> {
let bytes = Vec::<u8>::consensus_decode_partial_from_finite_reader(reader, modules)?;
let value: GuardianMetadata = serde_json::from_slice(&bytes)
.map_err(|e| DecodeError::new_custom(anyhow::anyhow!("Invalid JSON: {e}")))?;
let signature = secp256k1::schnorr::Signature::consensus_decode_partial_from_finite_reader(
reader, modules,
)?;
Ok(Self {
bytes,
value,
signature,
})
}
}
fn compute_tagged_hash(json_bytes: &[u8]) -> sha256::Hash {
use bitcoin::hashes::HashEngine;
let mut engine = sha256::HashEngine::default();
engine.input(GUARDIAN_METADATA_MESSAGE_TAG);
engine.input(json_bytes);
sha256::Hash::from_engine(engine)
}
#[derive(Debug, thiserror::Error)]
pub enum VerificationError {
#[error("Invalid signature")]
InvalidSignature,
#[error("Timestamp {timestamp_secs} is too far in the future (max allowed: {max_allowed})")]
TimestampTooFarInFuture {
timestamp_secs: u64,
max_allowed: u64,
},
}
impl GuardianMetadata {
pub fn new(api_urls: Vec<SafeUrl>, pkarr_id_z32: String, timestamp_secs: u64) -> Self {
Self {
api_urls,
pkarr_id_z32,
timestamp_secs,
}
}
pub fn sign<C: secp256k1::Signing>(
&self,
ctx: &secp256k1::Secp256k1<C>,
key: &secp256k1::Keypair,
) -> SignedGuardianMetadata {
let bytes = serde_json::to_vec(self).expect("JSON serialization should not fail");
let tagged_hash = compute_tagged_hash(&bytes);
let msg = Message::from_digest(*tagged_hash.as_ref());
let signature = ctx.sign_schnorr(&msg, key);
SignedGuardianMetadata {
bytes,
value: self.clone(),
signature,
}
}
}
impl SignedGuardianMetadata {
pub fn guardian_metadata(&self) -> &GuardianMetadata {
&self.value
}
pub fn tagged_hash(&self) -> sha256::Hash {
compute_tagged_hash(&self.bytes)
}
pub fn verify<C: secp256k1::Verification>(
&self,
ctx: &secp256k1::Secp256k1<C>,
pk: &secp256k1::PublicKey,
now: std::time::Duration,
) -> Result<(), VerificationError> {
let msg = Message::from_digest(*self.tagged_hash().as_ref());
ctx.verify_schnorr(&self.signature, &msg, &pk.x_only_public_key().0)
.map_err(|_| VerificationError::InvalidSignature)?;
let current_secs = now.as_secs();
let max_allowed_timestamp = current_secs.saturating_add(MAX_FUTURE_TIMESTAMP_SECS);
if max_allowed_timestamp < self.value.timestamp_secs {
return Err(VerificationError::TimestampTooFarInFuture {
timestamp_secs: self.value.timestamp_secs,
max_allowed: max_allowed_timestamp,
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::module::registry::ModuleRegistry;
#[test]
fn signed_guardian_metadata_json_roundtrip() {
let ctx = secp256k1::Secp256k1::new();
let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
let public_key = secp256k1::PublicKey::from_keypair(&keypair);
let timestamp_secs = 1000;
let metadata = GuardianMetadata::new(
vec!["wss://example.com/api".parse().unwrap()],
"test_pkarr_id".to_string(),
timestamp_secs,
);
let signed = metadata.sign(&ctx, &keypair);
let json = serde_json::to_string(&signed).expect("serialization should succeed");
let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
json_value.get("content").is_some(),
"should have content field"
);
assert!(
json_value.get("signature").is_some(),
"should have signature field"
);
let deserialized: SignedGuardianMetadata =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(signed.bytes, deserialized.bytes);
assert_eq!(signed.value, deserialized.value);
assert_eq!(signed.signature, deserialized.signature);
assert_eq!(signed, deserialized);
let now = Duration::from_secs(timestamp_secs);
deserialized
.verify(&ctx, &public_key, now)
.expect("signature should verify after roundtrip");
assert_eq!(*deserialized.guardian_metadata(), metadata);
}
#[test]
fn signed_guardian_metadata_encodable_roundtrip() {
let ctx = secp256k1::Secp256k1::new();
let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
let public_key = secp256k1::PublicKey::from_keypair(&keypair);
let timestamp_secs = 1000;
let metadata = GuardianMetadata::new(
vec!["wss://example.com/api".parse().unwrap()],
"test_pkarr_id".to_string(),
timestamp_secs,
);
let signed = metadata.sign(&ctx, &keypair);
let encoded = signed.consensus_encode_to_vec();
let deserialized: SignedGuardianMetadata =
Decodable::consensus_decode_whole(&encoded, &ModuleRegistry::default())
.expect("decoding should succeed");
assert_eq!(signed.bytes, deserialized.bytes);
assert_eq!(signed.value, deserialized.value);
assert_eq!(signed.signature, deserialized.signature);
assert_eq!(signed, deserialized);
let now = Duration::from_secs(timestamp_secs);
deserialized
.verify(&ctx, &public_key, now)
.expect("signature should verify after roundtrip");
assert_eq!(*deserialized.guardian_metadata(), metadata);
}
#[test]
fn verify_valid_signature_and_timestamp() {
let ctx = secp256k1::Secp256k1::new();
let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
let public_key = secp256k1::PublicKey::from_keypair(&keypair);
let timestamp_secs = 10000;
let metadata = GuardianMetadata::new(
vec!["wss://example.com/api".parse().unwrap()],
"test_pkarr_id".to_string(),
timestamp_secs,
);
let signed = metadata.sign(&ctx, &keypair);
signed
.verify(&ctx, &public_key, Duration::from_secs(timestamp_secs))
.expect("should verify with matching timestamp");
signed
.verify(
&ctx,
&public_key,
Duration::from_secs(timestamp_secs + 1000),
)
.expect("should verify with past timestamp");
signed
.verify(
&ctx,
&public_key,
Duration::from_secs(timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS),
)
.expect("should verify when timestamp is within allowed future window");
}
#[test]
fn verify_rejects_invalid_signature() {
let ctx = secp256k1::Secp256k1::new();
let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
let wrong_keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
let wrong_public_key = secp256k1::PublicKey::from_keypair(&wrong_keypair);
let timestamp_secs = 1000;
let metadata = GuardianMetadata::new(
vec!["wss://example.com/api".parse().unwrap()],
"test_pkarr_id".to_string(),
timestamp_secs,
);
let signed = metadata.sign(&ctx, &keypair);
let result = signed.verify(&ctx, &wrong_public_key, Duration::from_secs(timestamp_secs));
assert!(
matches!(result, Err(VerificationError::InvalidSignature)),
"should reject invalid signature"
);
}
#[test]
fn verify_rejects_timestamp_too_far_in_future() {
let ctx = secp256k1::Secp256k1::new();
let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
let public_key = secp256k1::PublicKey::from_keypair(&keypair);
let timestamp_secs = 10000;
let metadata = GuardianMetadata::new(
vec!["wss://example.com/api".parse().unwrap()],
"test_pkarr_id".to_string(),
timestamp_secs,
);
let signed = metadata.sign(&ctx, &keypair);
let now_secs = timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS - 1;
let result = signed.verify(&ctx, &public_key, Duration::from_secs(now_secs));
assert!(
matches!(
result,
Err(VerificationError::TimestampTooFarInFuture {
timestamp_secs: ts,
..
}) if ts == timestamp_secs
),
"should reject timestamp too far in future"
);
}
}