cdk 0.16.0

Core Cashu Development Kit library implementing the Cashu protocol
Documentation
use std::collections::HashSet;

use cdk_common::{Amount, BlindedMessage, CurrencyUnit, Id, Proofs, ProofsMethods, PublicKey};
use tracing::instrument;

use super::{Error, Mint};

/// Maximum allowed length in bytes for proof secret or witness content
const MAX_PROOF_CONTENT_LEN: usize = 1024;

/// Maximum allowed length in bytes for request fields (description, extra)
pub(crate) const MAX_REQUEST_FIELD_LEN: usize = 1024;

/// Verification result with typed amount
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Verification {
    /// Verified amount with unit
    pub amount: Amount<CurrencyUnit>,
}

impl Mint {
    /// Verify that the inputs to the transaction are unique
    #[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(())
    }

    /// Verify that the outputs to are unique
    #[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(())
    }

    /// Verify output keyset
    ///
    /// Checks that the outputs are all of the same unit and the keyset is active
    #[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);
                }
            }
        }

        // Check that all proofs are the same unit
        // in the future it maybe possible to support multiple units but unsupported for
        // now
        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)
    }

    /// Verify input keyset
    ///
    /// Checks that the inputs are all of the same unit
    #[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);
                }
            }
        }

        // Check that all proofs are the same unit
        // in the future it maybe possible to support multiple units but unsupported for
        // now
        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)
    }

    /// Verifies outputs
    ///
    /// Checks outputs are unique, of the same unit and not signed before.
    /// Returns an error if outputs are empty - callers should guard against
    /// empty outputs before calling this function.
    #[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));
        }

        // Check max outputs limit
        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 })
    }

    /// Verifies inputs
    ///
    /// Checks that inputs are unique and of the same unit.
    /// **NOTE: This does not check if inputs have been spent
    #[instrument(skip_all)]
    pub async fn verify_inputs(&self, inputs: &Proofs) -> Result<Verification, Error> {
        // Check max inputs limit
        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,
            });
        }

        // Check proof content lengths (secret and witness) are within limits
        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 })
    }

    /// Verify that inputs and outputs are valid and balanced
    #[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?;

        // Units are now embedded in the typed amounts - check they match
        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);
        }

        // Check amounts are balanced (inputs = outputs + fee)
        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(())
    }
}