use bitcoin::secp256k1::schnorr::Signature;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use super::nut01::Error as Nut01Error;
use super::{PublicKey, SecretKey};
use crate::{Amount, BlindedMessage};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Signature(#[from] super::nut20::Error),
#[error(transparent)]
Nut01(#[from] Nut01Error),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct Settings {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_batch_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub methods: Option<Vec<String>>,
}
impl Settings {
pub fn new(max_batch_size: Option<u64>, methods: Option<Vec<String>>) -> Self {
Self {
max_batch_size,
methods,
}
}
pub fn is_empty(&self) -> bool {
self.max_batch_size.is_none() && self.methods.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct BatchCheckMintQuoteRequest<Q> {
pub quotes: Vec<Q>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct BatchMintRequest<Q> {
pub quotes: Vec<Q>,
pub quote_amounts: Option<Vec<Amount>>,
pub outputs: Vec<BlindedMessage>,
pub signatures: Option<Vec<Option<String>>>,
}
impl<Q> BatchMintRequest<Q>
where
Q: ToString,
{
pub fn msg_to_sign(&self, quote: &Q) -> Vec<u8> {
let quote_id = quote.to_string();
let capacity = quote_id.len() + (self.outputs.len() * 66);
let mut msg = Vec::with_capacity(capacity);
msg.extend_from_slice(quote_id.as_bytes());
for output in &self.outputs {
msg.extend_from_slice(output.blinded_secret.to_hex().as_bytes());
}
msg
}
pub fn sign_quote(&self, quote: &Q, secret_key: &SecretKey) -> Result<String, Error> {
let msg = self.msg_to_sign(quote);
let signature: Signature = secret_key.sign(&msg)?;
Ok(signature.to_string())
}
pub fn verify_quote_signature(
&self,
quote: &Q,
signature: &str,
pubkey: &PublicKey,
) -> Result<(), Error> {
let signature = signature
.parse::<Signature>()
.map_err(|_| super::nut20::Error::InvalidSignature)?;
let msg = self.msg_to_sign(quote);
pubkey
.verify(&msg, &signature)
.map_err(|_| super::nut20::Error::InvalidSignature)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::Id;
fn dummy_blinded_message() -> BlindedMessage {
let secret_key = SecretKey::generate();
BlindedMessage {
amount: Amount::from(1),
keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
blinded_secret: secret_key.public_key(),
witness: None,
}
}
#[test]
fn test_sign_and_verify_batch_quote_roundtrip() {
let secret_key = SecretKey::generate();
let pubkey = secret_key.public_key();
let quote_id = "test-quote-id-123".to_string();
let outputs = vec![dummy_blinded_message(), dummy_blinded_message()];
let request = BatchMintRequest {
quotes: vec![quote_id.clone()],
quote_amounts: None,
outputs,
signatures: None,
};
let signature = request
.sign_quote("e_id, &secret_key)
.expect("signing should succeed");
request
.verify_quote_signature("e_id, &signature, &pubkey)
.expect("verification should succeed with correct key");
}
#[test]
fn test_verify_batch_quote_wrong_key() {
let signing_key = SecretKey::generate();
let wrong_key = SecretKey::generate();
let quote_id = "test-quote-wrong-key".to_string();
let outputs = vec![dummy_blinded_message()];
let request = BatchMintRequest {
quotes: vec![quote_id.clone()],
quote_amounts: None,
outputs,
signatures: None,
};
let signature = request
.sign_quote("e_id, &signing_key)
.expect("signing should succeed");
let result = request.verify_quote_signature("e_id, &signature, &wrong_key.public_key());
assert!(result.is_err(), "verification should fail with wrong key");
}
#[test]
fn test_verify_batch_quote_tampered_outputs() {
let secret_key = SecretKey::generate();
let pubkey = secret_key.public_key();
let quote_id = "test-quote-tampered".to_string();
let outputs = vec![dummy_blinded_message(), dummy_blinded_message()];
let request = BatchMintRequest {
quotes: vec![quote_id.clone()],
quote_amounts: None,
outputs,
signatures: None,
};
let signature = request
.sign_quote("e_id, &secret_key)
.expect("signing should succeed");
let tampered_request = BatchMintRequest {
quotes: vec![quote_id.clone()],
quote_amounts: None,
outputs: vec![dummy_blinded_message(), dummy_blinded_message()],
signatures: None,
};
let result = tampered_request.verify_quote_signature("e_id, &signature, &pubkey);
assert!(
result.is_err(),
"verification should fail with tampered outputs"
);
}
#[test]
fn test_verify_batch_quote_wrong_quote_id() {
let secret_key = SecretKey::generate();
let pubkey = secret_key.public_key();
let quote_id = "original-quote-id".to_string();
let outputs = vec![dummy_blinded_message()];
let request = BatchMintRequest {
quotes: vec![quote_id.clone()],
quote_amounts: None,
outputs,
signatures: None,
};
let signature = request
.sign_quote("e_id, &secret_key)
.expect("signing should succeed");
let different_quote_id = "different-quote-id".to_string();
let result = request.verify_quote_signature(&different_quote_id, &signature, &pubkey);
assert!(
result.is_err(),
"verification should fail with different quote ID"
);
}
#[test]
fn test_sign_batch_quote_empty_outputs() {
let secret_key = SecretKey::generate();
let pubkey = secret_key.public_key();
let quote_id = "test-empty-outputs".to_string();
let request = BatchMintRequest {
quotes: vec![quote_id.clone()],
quote_amounts: None,
outputs: vec![],
signatures: None,
};
let signature = request
.sign_quote("e_id, &secret_key)
.expect("signing should succeed");
request
.verify_quote_signature("e_id, &signature, &pubkey)
.expect("verification should succeed even with empty outputs");
}
#[test]
fn test_sign_batch_quote_multiple_quotes_different_sigs() {
let secret_key = SecretKey::generate();
let outputs = vec![dummy_blinded_message(), dummy_blinded_message()];
let quote_1 = "quote-1".to_string();
let quote_2 = "quote-2".to_string();
let request = BatchMintRequest {
quotes: vec![quote_1.clone(), quote_2.clone()],
quote_amounts: None,
outputs,
signatures: None,
};
let sig1 = request
.sign_quote("e_1, &secret_key)
.expect("signing should succeed");
let sig2 = request
.sign_quote("e_2, &secret_key)
.expect("signing should succeed");
assert_ne!(
sig1, sig2,
"signatures for different quote IDs should differ"
);
}
}