use crate::{ConclaveError, ConclaveResult};
use bitcoin::secp256k1::{self, Scalar, Secp256k1, SecretKey};
use bitcoin::PublicKey;
use hex;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
pub struct FedimintAdapter {
pub federations: HashMap<String, PublicKey>,
_secp: Secp256k1<secp256k1::All>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianThreshold {
pub total_guardians: u32,
pub threshold: u32,
pub guardian_keys: Vec<String>,
}
impl GuardianThreshold {
pub fn new(total: u32, threshold: u32, keys: Vec<String>) -> ConclaveResult<Self> {
if threshold > total {
return Err(ConclaveError::CryptoError(
"Threshold cannot exceed total guardians".to_string(),
));
}
if keys.len() != total as usize {
return Err(ConclaveError::CryptoError(
"Number of keys must match total guardians".to_string(),
));
}
Ok(Self {
total_guardians: total,
threshold,
guardian_keys: keys,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DleqProof {
pub challenge: String,
pub response: String,
pub public_key: String,
pub commitment_a: String,
pub commitment_b: String,
}
impl DleqProof {
pub fn verify(&self) -> bool {
!self.challenge.is_empty()
&& !self.response.is_empty()
&& !self.public_key.is_empty()
&& !self.commitment_a.is_empty()
&& !self.commitment_b.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlindSignatureRequest {
pub blinded_message: String,
pub amount_sats: u64,
pub dleq_proof: DleqProof,
pub request_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartialBlindSignature {
pub guardian_id: u32,
pub signature_share: String,
pub public_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThresholdBlindSignature {
pub aggregated_signature: String,
pub signature_count: u32,
pub threshold: u32,
pub federation_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FedimintMintIntent {
pub amount_sats: u64,
pub federation_id: String,
pub blinded_messages: Vec<String>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FedimintEcash {
pub notes: Vec<EcashNote>,
pub total_amount: u64,
pub proof_of_reserve: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EcashNote {
pub federation_id: String,
pub amount: u64,
pub secret: String, pub signature: String, }
impl Default for FedimintAdapter {
fn default() -> Self {
Self::new()
}
}
impl FedimintAdapter {
pub fn new() -> Self {
Self {
federations: HashMap::new(),
_secp: Secp256k1::new(),
}
}
pub fn register_federation(&mut self, federation_id: &str) -> ConclaveResult<()> {
let mut fed_sk_hasher = Sha256::new();
fed_sk_hasher.update(federation_id.as_bytes());
let fed_sk_hash = fed_sk_hasher.finalize();
let fed_sk_bytes: [u8; 32] = fed_sk_hash.into();
let fed_sk = SecretKey::from_secret_bytes(fed_sk_bytes).map_err(|_| {
ConclaveError::CryptoError("Failed to derive federation key".to_string())
})?;
let fed_pk_internal = secp256k1::PublicKey::from_secret_key(&fed_sk);
let fed_pk = PublicKey::from_secp(fed_pk_internal);
self.federations.insert(federation_id.to_string(), fed_pk);
Ok(())
}
pub fn join_federation(&mut self, invite_code: &str) -> ConclaveResult<String> {
if !invite_code.starts_with("fed1") {
return Err(ConclaveError::InvalidPayload);
}
let mut hasher = Sha256::new();
hasher.update(invite_code.as_bytes());
let fed_id = hex::encode(&hasher.finalize()[0..8]);
self.register_federation(&fed_id)?;
Ok(fed_id)
}
pub fn prepare_mint_intent(
&self,
federation_id: &str,
amount_sats: u64,
secrets: Vec<&str>,
) -> ConclaveResult<(FedimintMintIntent, Vec<String>)> {
if !self.federations.contains_key(federation_id) {
return Err(ConclaveError::RailError(
"Federation not registered".to_string(),
));
}
if amount_sats == 0 || secrets.is_empty() {
return Err(ConclaveError::InvalidPayload);
}
let mut blinded_messages = Vec::new();
let mut blinding_factors = Vec::new();
for secret_str in secrets.iter() {
let mut hasher = Sha256::new();
hasher.update(secret_str.as_bytes());
let secret_hash = hasher.finalize();
let sk_bytes: [u8; 32] = secret_hash.into();
let sk = SecretKey::from_secret_bytes(sk_bytes)
.map_err(|_| ConclaveError::CryptoError("Invalid secret hash".to_string()))?;
let pk_internal = secp256k1::PublicKey::from_secret_key(&sk);
let mut r_hasher = Sha256::new();
r_hasher.update(secret_str.as_bytes());
r_hasher.update(b"BLINDING_FACTOR_v2");
let r_hash = r_hasher.finalize();
let r_bytes: [u8; 32] = r_hash.into();
blinding_factors.push(hex::encode(r_bytes));
let r_scalar = Scalar::from_be_bytes(r_bytes)
.map_err(|_| ConclaveError::CryptoError("Invalid blinding factor".to_string()))?;
let blinded_pk = pk_internal
.mul_tweak(&r_scalar)
.map_err(|e| ConclaveError::CryptoError(format!("Blinding failed: {:?}", e)))?;
blinded_messages.push(hex::encode(blinded_pk.serialize()));
}
Ok((
FedimintMintIntent {
amount_sats,
federation_id: federation_id.to_string(),
blinded_messages,
},
blinding_factors,
))
}
pub fn issue_ecash(
&self,
intent: FedimintMintIntent,
blinding_factors: Vec<String>,
original_secrets: Vec<String>,
) -> ConclaveResult<FedimintEcash> {
if !self.federations.contains_key(&intent.federation_id) {
return Err(ConclaveError::RailError(
"Federation not registered".to_string(),
));
}
if intent.blinded_messages.len() != blinding_factors.len() {
return Err(ConclaveError::InvalidPayload);
}
let mut notes = Vec::new();
let amount_per_note = intent.amount_sats / intent.blinded_messages.len() as u64;
let mut fed_sk_hasher = Sha256::new();
fed_sk_hasher.update(intent.federation_id.as_bytes());
let fed_sk_hash = fed_sk_hasher.finalize();
let fed_sk_bytes: [u8; 32] = fed_sk_hash.into();
let fed_sk = SecretKey::from_secret_bytes(fed_sk_bytes)
.map_err(|_| ConclaveError::CryptoError("Invalid federation key".to_string()))?;
let fed_scalar = Scalar::from_be_bytes(fed_sk.to_secret_bytes())
.map_err(|_| ConclaveError::CryptoError("Invalid federation scalar".to_string()))?;
for (i, _msg_hex) in intent.blinded_messages.iter().enumerate() {
let mut h = Sha256::new();
h.update(original_secrets[i].as_bytes());
let s_hash = h.finalize();
let sk_b: [u8; 32] = s_hash.into();
let sk = SecretKey::from_secret_bytes(sk_b)
.map_err(|_| ConclaveError::CryptoError("Invalid secret key".to_string()))?;
let pk_internal = secp256k1::PublicKey::from_secret_key(&sk);
let unblinded_sig = pk_internal.mul_tweak(&fed_scalar).map_err(|_| {
ConclaveError::CryptoError("Signature computation failed".to_string())
})?;
notes.push(EcashNote {
federation_id: intent.federation_id.clone(),
amount: amount_per_note,
secret: original_secrets[i].clone(),
signature: hex::encode(unblinded_sig.serialize()),
});
}
Ok(FedimintEcash {
notes,
total_amount: intent.amount_sats,
proof_of_reserve: Some(hex::encode(intent.amount_sats.to_be_bytes())),
})
}
pub fn verify_note(&self, note: &EcashNote) -> bool {
if !self.federations.contains_key(¬e.federation_id) {
return false;
};
let mut hasher = Sha256::new();
hasher.update(note.secret.as_bytes());
let secret_hash = hasher.finalize();
let sk_bytes: [u8; 32] = secret_hash.into();
let sk = match SecretKey::from_secret_bytes(sk_bytes) {
Ok(k) => k,
Err(_) => return false,
};
let pk_internal = secp256k1::PublicKey::from_secret_key(&sk);
let mut fed_sk_hasher = Sha256::new();
fed_sk_hasher.update(note.federation_id.as_bytes());
let fed_sk_hash = fed_sk_hasher.finalize();
let fed_sk_bytes: [u8; 32] = fed_sk_hash.into();
let fed_sk = match SecretKey::from_secret_bytes(fed_sk_bytes) {
Ok(k) => k,
Err(_) => return false,
};
let fed_scalar = match Scalar::from_be_bytes(fed_sk.to_secret_bytes()) {
Ok(s) => s,
Err(_) => return false,
};
let expected_sig = match pk_internal.mul_tweak(&fed_scalar) {
Ok(s) => s,
Err(_) => return false,
};
note.signature == hex::encode(expected_sig.serialize())
}
pub fn create_dleq_proof(
&self,
secret: &str,
public_key_hex: &str,
commitment_a: &str,
commitment_b: &str,
) -> ConclaveResult<DleqProof> {
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
let secret_hash = hasher.finalize();
let sk_bytes: [u8; 32] = secret_hash.into();
let mut challenge_hasher = Sha256::new();
challenge_hasher.update(b"G"); challenge_hasher.update(public_key_hex.as_bytes());
challenge_hasher.update(commitment_a.as_bytes());
challenge_hasher.update(commitment_b.as_bytes());
challenge_hasher.update(secret.as_bytes());
let challenge = hex::encode(challenge_hasher.finalize());
let mut response_hasher = Sha256::new();
response_hasher.update(challenge.as_bytes());
response_hasher.update(sk_bytes);
let response = hex::encode(response_hasher.finalize());
Ok(DleqProof {
challenge,
response,
public_key: public_key_hex.to_string(),
commitment_a: commitment_a.to_string(),
commitment_b: commitment_b.to_string(),
})
}
pub fn create_blind_signature_request(
&self,
blinded_message: &str,
amount_sats: u64,
dleq_proof: DleqProof,
) -> ConclaveResult<BlindSignatureRequest> {
if !dleq_proof.verify() {
return Err(ConclaveError::CryptoError(
"Invalid DLEQ proof structure".to_string(),
));
}
let mut id_hasher = Sha256::new();
id_hasher.update(blinded_message.as_bytes());
id_hasher.update(amount_sats.to_be_bytes());
id_hasher.update(dleq_proof.challenge.as_bytes());
let request_id = hex::encode(&id_hasher.finalize()[0..16]);
Ok(BlindSignatureRequest {
blinded_message: blinded_message.to_string(),
amount_sats,
dleq_proof,
request_id,
})
}
pub fn aggregate_threshold_signatures(
&self,
partial_signatures: Vec<PartialBlindSignature>,
threshold: u32,
federation_id: &str,
) -> ConclaveResult<ThresholdBlindSignature> {
if partial_signatures.len() < threshold as usize {
return Err(ConclaveError::CryptoError(format!(
"Not enough signatures: got {}, need {}",
partial_signatures.len(),
threshold
)));
}
let mut agg_hasher = Sha256::new();
for sig in &partial_signatures {
agg_hasher.update(sig.signature_share.as_bytes());
}
let aggregated = hex::encode(agg_hasher.finalize());
Ok(ThresholdBlindSignature {
aggregated_signature: aggregated,
signature_count: partial_signatures.len() as u32,
threshold,
federation_id: federation_id.to_string(),
})
}
pub fn validate_threshold_signature(&self, signature: &ThresholdBlindSignature) -> bool {
signature.signature_count >= signature.threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fedimint_mint_flow() {
let mut adapter = FedimintAdapter::new();
adapter.register_federation("fed-1").unwrap();
let secrets = vec!["secret1", "secret2"];
let (intent, blinding_factors) = adapter
.prepare_mint_intent("fed-1", 1000, secrets.clone())
.unwrap();
assert_eq!(intent.blinded_messages.len(), 2);
let ecash = adapter
.issue_ecash(
intent,
blinding_factors,
secrets.iter().map(|s| s.to_string()).collect(),
)
.unwrap();
assert_eq!(ecash.total_amount, 1000);
assert_eq!(ecash.notes.len(), 2);
assert!(adapter.verify_note(&ecash.notes[0]));
assert!(adapter.verify_note(&ecash.notes[1]));
}
#[test]
fn test_fedimint_join_federation() {
let mut adapter = FedimintAdapter::new();
let fed_id = adapter.join_federation("fed1_example_invite").unwrap();
assert!(adapter.federations.contains_key(&fed_id));
}
#[test]
fn test_fedimint_multiple_federations() {
let mut adapter = FedimintAdapter::new();
adapter.register_federation("fed-1").unwrap();
adapter.register_federation("fed-2").unwrap();
let secrets1 = vec!["s1"];
let (intent1, bf1) = adapter
.prepare_mint_intent("fed-1", 100, secrets1.clone())
.unwrap();
let ecash1 = adapter
.issue_ecash(intent1, bf1, vec!["s1".to_string()])
.unwrap();
let secrets2 = vec!["s2"];
let (intent2, bf2) = adapter
.prepare_mint_intent("fed-2", 200, secrets2.clone())
.unwrap();
let ecash2 = adapter
.issue_ecash(intent2, bf2, vec!["s2".to_string()])
.unwrap();
assert!(adapter.verify_note(&ecash1.notes[0]));
assert!(adapter.verify_note(&ecash2.notes[0]));
assert_eq!(ecash1.notes[0].federation_id, "fed-1");
assert_eq!(ecash2.notes[0].federation_id, "fed-2");
}
#[test]
fn test_fedimint_invalid_federation() {
let mut adapter = FedimintAdapter::new();
adapter.register_federation("fed-1").unwrap();
let intent = FedimintMintIntent {
amount_sats: 1000,
federation_id: "fed-2".to_string(),
blinded_messages: vec!["msg1".to_string()],
};
let result = adapter.issue_ecash(intent, vec!["bf1".into()], vec!["s1".into()]);
assert!(result.is_err());
}
}