use bitcoin::hashes::{Hash, sha256};
use bitcoin::secp256k1::Message;
use fedimint_core::encoding::{Decodable, DecodeContext as _, 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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iroh_next_endpoint: Option<String>,
}
#[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(DecodeError::from_err)
.context("Invalid JSON")?;
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,
iroh_next_endpoint: None,
}
}
pub fn with_iroh_next_endpoint(mut self, endpoint: String) -> Self {
self.iroh_next_endpoint = Some(endpoint);
self
}
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;