use std::collections::HashSet;
use cdk_common::{Amount, BlindedMessage, CurrencyUnit, Id, Proofs, ProofsMethods, PublicKey};
use tracing::instrument;
use super::{Error, Mint};
const MAX_PROOF_CONTENT_LEN: usize = 1024;
pub(crate) const MAX_REQUEST_FIELD_LEN: usize = 1024;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Verification {
pub amount: Amount<CurrencyUnit>,
}
impl Mint {
#[instrument(skip_all)]
pub fn check_inputs_unique(inputs: &Proofs) -> Result<(), Error> {
let proof_count = inputs.len();
if inputs
.iter()
.map(|i| i.y())
.collect::<Result<HashSet<PublicKey>, _>>()?
.len()
.ne(&proof_count)
{
tracing::debug!("Transaction attempted with duplicate inputs");
return Err(Error::DuplicateInputs);
}
Ok(())
}
#[instrument(skip_all)]
pub fn check_outputs_unique(outputs: &[BlindedMessage]) -> Result<(), Error> {
let output_count = outputs.len();
if outputs
.iter()
.map(|o| &o.blinded_secret)
.collect::<HashSet<&PublicKey>>()
.len()
.ne(&output_count)
{
tracing::debug!("Transaction attempted with duplicate outputs");
return Err(Error::DuplicateOutputs);
}
Ok(())
}
#[instrument(skip_all)]
pub fn verify_outputs_keyset(&self, outputs: &[BlindedMessage]) -> Result<CurrencyUnit, Error> {
let mut keyset_units = HashSet::new();
let output_keyset_ids: HashSet<Id> = outputs.iter().map(|p| p.keyset_id).collect();
for id in &output_keyset_ids {
match self.get_keyset_info(id) {
Some(keyset) => {
if !keyset.active {
tracing::debug!(
"Transaction attempted with inactive keyset in outputs: {}.",
id
);
return Err(Error::InactiveKeyset);
}
keyset_units.insert(keyset.unit);
}
None => {
tracing::debug!(
"Transaction attempted with unknown keyset in outputs: {}.",
id
);
return Err(Error::UnknownKeySet);
}
}
}
if keyset_units.len() != 1 {
tracing::debug!(
"Transaction attempted with multiple units in outputs: {:?}.",
keyset_units
);
return Err(Error::MultipleUnits);
}
keyset_units.into_iter().next().ok_or(Error::Internal)
}
#[instrument(skip_all)]
pub async fn verify_inputs_keyset(&self, inputs: &Proofs) -> Result<CurrencyUnit, Error> {
let mut keyset_units = HashSet::new();
let inputs_keyset_ids: HashSet<Id> = inputs.iter().map(|p| p.keyset_id).collect();
for id in &inputs_keyset_ids {
match self.get_keyset_info(id) {
Some(keyset) => {
keyset_units.insert(keyset.unit);
}
None => {
tracing::debug!(
"Transaction attempted with unknown keyset in inputs: {}.",
id
);
return Err(Error::UnknownKeySet);
}
}
}
if keyset_units.len() != 1 {
tracing::debug!(
"Transaction attempted with multiple units in inputs: {:?}.",
keyset_units
);
return Err(Error::MultipleUnits);
}
keyset_units.into_iter().next().ok_or(Error::Internal)
}
#[instrument(skip_all)]
pub fn verify_outputs(&self, outputs: &[BlindedMessage]) -> Result<Verification, Error> {
if outputs.is_empty() {
tracing::debug!("verify_outputs called with empty outputs");
return Err(Error::TransactionUnbalanced(0, 0, 0));
}
let outputs_count = outputs.len();
if outputs_count > self.max_outputs {
tracing::warn!(
"Mint request exceeds max outputs limit: {} > {}",
outputs_count,
self.max_outputs
);
return Err(Error::MaxOutputsExceeded {
actual: outputs_count,
max: self.max_outputs,
});
}
Mint::check_outputs_unique(outputs)?;
let unit = self.verify_outputs_keyset(outputs)?;
let amount = Amount::try_sum(outputs.iter().map(|o| o.amount))?.with_unit(unit);
Ok(Verification { amount })
}
#[instrument(skip_all)]
pub async fn verify_inputs(&self, inputs: &Proofs) -> Result<Verification, Error> {
let inputs_count = inputs.len();
if inputs_count > self.max_inputs {
tracing::warn!(
"Melt request exceeds max inputs limit: {} > {}",
inputs_count,
self.max_inputs
);
return Err(Error::MaxInputsExceeded {
actual: inputs_count,
max: self.max_inputs,
});
}
for proof in inputs {
let secret_len = proof.secret.len();
if secret_len > MAX_PROOF_CONTENT_LEN {
tracing::warn!(
"Proof secret exceeds max content length: {} > {}",
secret_len,
MAX_PROOF_CONTENT_LEN
);
return Err(Error::ProofContentTooLarge {
actual: secret_len,
max: MAX_PROOF_CONTENT_LEN,
});
}
if let Some(witness) = &proof.witness {
let witness_str = serde_json::to_string(witness)?;
let witness_len = witness_str.len();
if witness_len > MAX_PROOF_CONTENT_LEN {
tracing::warn!(
"Proof witness exceeds max content length: {} > {}",
witness_len,
MAX_PROOF_CONTENT_LEN
);
return Err(Error::ProofContentTooLarge {
actual: witness_len,
max: MAX_PROOF_CONTENT_LEN,
});
}
}
}
Mint::check_inputs_unique(inputs)?;
let unit = self.verify_inputs_keyset(inputs).await?;
if unit == CurrencyUnit::Auth {
return Err(Error::UnsupportedUnit);
}
let amount = inputs.total_amount()?.with_unit(unit);
self.verify_proofs(inputs.clone()).await?;
Ok(Verification { amount })
}
#[instrument(skip_all)]
pub async fn verify_transaction_balanced(
&self,
input_verification: Verification,
output_verification: Verification,
inputs: &Proofs,
) -> Result<(), Error> {
let fee_breakdown = self.get_proofs_fee(inputs).await?;
if output_verification.amount.unit() != input_verification.amount.unit() {
tracing::debug!(
"Output unit {:?} does not match input unit {:?}",
output_verification.amount.unit(),
input_verification.amount.unit()
);
return Err(Error::UnitMismatch);
}
let fee_typed = fee_breakdown
.total
.with_unit(input_verification.amount.unit().clone());
let expected_output = input_verification.amount.checked_sub(&fee_typed)?;
if output_verification.amount != expected_output {
return Err(Error::TransactionUnbalanced(
input_verification.amount.value(),
output_verification.amount.value(),
fee_breakdown.total.into(),
));
}
Ok(())
}
}