nym-credentials-interface 1.20.5

Interface for Nym's compact eacash / zknym credential scheme
Documentation
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0

use rand::Rng;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use thiserror::Error;
use time::{Date, OffsetDateTime};

pub use nym_compact_ecash::{
    Base58, BlindedSignature, Bytable, EncodedDate, EncodedTicketType, PartialWallet, PayInfo,
    PublicKeyUser, SecretKeyUser, VerificationKeyAuth, WithdrawalRequest,
    aggregate_verification_keys, aggregate_wallets, constants, ecash_parameters,
    error::CompactEcashError,
    generate_keypair_user, generate_keypair_user_from_seed, issue_verify,
    scheme::Payment,
    scheme::coin_indices_signatures::aggregate_indices_signatures,
    scheme::coin_indices_signatures::{
        AnnotatedCoinIndexSignature, CoinIndexSignature, CoinIndexSignatureShare,
        PartialCoinIndexSignature,
    },
    scheme::expiration_date_signatures::aggregate_expiration_signatures,
    scheme::expiration_date_signatures::{
        AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare,
        PartialExpirationDateSignature,
    },
    scheme::keygen::KeyPairUser,
    scheme::withdrawal::RequestInfo,
    scheme::{Wallet, WalletSignatures},
    withdrawal_request,
};
pub use nym_ecash_time::{EcashTime, ecash_today};
pub use nym_network_defaults::TicketTypeRepr;
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;

/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms
/// to increase their allowance.
// currently defined as 20% of entry ticket value
// clients are, of course, free to override this value
pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 =
    (V1MixnetEntry.bandwidth_value() / 5) as i64;

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum BandwidthCredential {
    ZkNym(Box<CredentialSpendingData>),
    UpgradeModeJWT { token: String },
}

impl BandwidthCredential {
    pub fn into_zk_nym(self) -> Option<Box<CredentialSpendingData>> {
        match self {
            BandwidthCredential::ZkNym(credential) => Some(credential),
            _ => None,
        }
    }
}

impl From<CredentialSpendingData> for BandwidthCredential {
    fn from(credential: CredentialSpendingData) -> Self {
        Self::ZkNym(Box::new(credential))
    }
}

#[derive(Debug, Clone)]
pub struct CredentialSigningData {
    pub withdrawal_request: WithdrawalRequest,

    pub request_info: RequestInfo,

    pub ecash_pub_key: PublicKeyUser,

    pub expiration_date: Date,

    pub ticketbook_type: TicketType,
}

#[derive(Serialize, Deserialize, PartialEq, Clone)]
pub struct CredentialSpendingData {
    pub payment: Payment,

    pub pay_info: PayInfo,

    pub spend_date: Date,

    // pub value: u64,
    /// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation.
    pub epoch_id: u64,
}

impl Debug for CredentialSpendingData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // we're redacting the payment not since it contains secret,
        // but because it's producing a lot of noise in the output and
        // we are not really interested in coordinates of each of the attached curve points
        f.debug_struct("CredentialSpendingData")
            .field("payment", &"[REDACTED]")
            .field("pay_info", &self.pay_info)
            .field("spend_date", &self.spend_date)
            .field("epoch_id", &self.epoch_id)
            .finish()
    }
}

impl CredentialSpendingData {
    pub fn verify(&self, verification_key: &VerificationKeyAuth) -> Result<(), CompactEcashError> {
        self.payment.spend_verify(
            verification_key,
            &self.pay_info,
            self.spend_date.ecash_unix_timestamp(),
        )
    }

    pub fn encoded_serial_number(&self) -> Vec<u8> {
        self.payment.encoded_serial_number()
    }

    pub fn serial_number_b58(&self) -> String {
        self.payment.serial_number_bs58()
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        // simple length prefixed serialization
        // TODO: change it to a standard format instead
        let mut bytes = Vec::new();
        let payment_bytes = self.payment.to_bytes();

        bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes());
        bytes.extend_from_slice(&payment_bytes);
        bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long
        bytes.extend_from_slice(&self.spend_date.to_julian_day().to_be_bytes());
        bytes.extend_from_slice(&self.epoch_id.to_be_bytes());

        bytes
    }

    pub fn try_from_bytes(raw: &[u8]) -> Result<Self, CompactEcashError> {
        // minimum length: 72 (pay_info) + 8 (epoch_id) + 4 (spend date) + 4 (payment length prefix)
        if raw.len() < 72 + 8 + 4 + 4 {
            return Err(CompactEcashError::DeserializationFailure {
                object: "EcashCredential".into(),
            });
        }
        let mut index = 0;
        //SAFETY : casting a slice of length 4 into an array of size 4
        let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize;
        index += 4;

        if raw[index..].len() != payment_len + 84 {
            return Err(CompactEcashError::DeserializationFailure {
                object: "EcashCredential".into(),
            });
        }
        let payment = Payment::try_from(&raw[index..index + payment_len])?;
        index += payment_len;

        let pay_info = PayInfo {
            //SAFETY : casting a slice of length 72 into an array of size 72
            pay_info_bytes: raw[index..index + 72].try_into().unwrap(),
        };
        index += 72;

        //SAFETY : casting a slice of length 4 into an array of size 4
        let spend_date_julian = i32::from_be_bytes(raw[index..index + 4].try_into().unwrap());
        let spend_date = Date::from_julian_day(spend_date_julian).map_err(|_| {
            CompactEcashError::DeserializationFailure {
                object: "CredentialSpendingData".into(),
            }
        })?;
        index += 4;

        if raw[index..].len() != 8 {
            return Err(CompactEcashError::DeserializationFailure {
                object: "EcashCredential".into(),
            });
        }

        //SAFETY : casting a slice of length 8 into an array of size 8
        let epoch_id = u64::from_be_bytes(raw[index..].try_into().unwrap());

        Ok(CredentialSpendingData {
            payment,
            pay_info,
            spend_date,
            epoch_id,
        })
    }
}

impl Bytable for CredentialSpendingData {
    fn to_byte_vec(&self) -> Vec<u8> {
        self.to_bytes()
    }

    fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
        Self::try_from_bytes(slice)
    }
}

impl Base58 for CredentialSpendingData {}

#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub struct NymPayInfo {
    randomness: [u8; 32],
    timestamp: i64,
    provider_public_key: [u8; 32],
}

impl NymPayInfo {
    /// Generates a new `NymPayInfo` instance with random bytes, a timestamp, and a provider public key.
    ///
    /// # Arguments
    ///
    /// * `provider_pk` - The public key of the payment provider.
    ///
    /// # Returns
    ///
    /// A new `NymPayInfo` instance.
    ///
    pub fn generate(provider_pk: [u8; 32]) -> Self {
        let mut randomness = [0u8; 32];
        rand::thread_rng().fill(&mut randomness[..32]);

        let timestamp = OffsetDateTime::now_utc().unix_timestamp();

        NymPayInfo {
            randomness,
            timestamp,
            provider_public_key: provider_pk,
        }
    }

    pub fn timestamp(&self) -> i64 {
        self.timestamp
    }

    pub fn pk(&self) -> [u8; 32] {
        self.provider_public_key
    }
}

impl From<NymPayInfo> for PayInfo {
    fn from(value: NymPayInfo) -> Self {
        let mut pay_info_bytes = [0u8; 72];

        pay_info_bytes[..32].copy_from_slice(&value.randomness);
        pay_info_bytes[32..40].copy_from_slice(&value.timestamp.to_be_bytes());
        pay_info_bytes[40..].copy_from_slice(&value.provider_public_key);

        PayInfo { pay_info_bytes }
    }
}

impl From<PayInfo> for NymPayInfo {
    fn from(value: PayInfo) -> Self {
        //SAFETY : slice to array of same length
        let randomness = value.pay_info_bytes[..32].try_into().unwrap();
        let timestamp = i64::from_be_bytes(value.pay_info_bytes[32..40].try_into().unwrap());
        let provider_public_key = value.pay_info_bytes[40..].try_into().unwrap();

        NymPayInfo {
            randomness,
            timestamp,
            provider_public_key,
        }
    }
}

#[derive(
    Copy,
    Clone,
    Debug,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    Hash,
    strum_macros::Display,
    strum_macros::EnumString,
    strum_macros::EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum TicketType {
    V1MixnetEntry,
    V1MixnetExit,
    V1WireguardEntry,
    V1WireguardExit,
}

#[derive(Debug, Copy, Clone, Error)]
#[error("provided unknown ticketbook type")]
pub struct UnknownTicketType;

impl TicketType {
    pub fn to_repr(&self) -> TicketTypeRepr {
        (*self).into()
    }

    pub fn encode(&self) -> EncodedTicketType {
        self.to_repr() as EncodedTicketType
    }

    pub fn try_from_encoded(val: EncodedTicketType) -> Result<Self, UnknownTicketType> {
        match val {
            n if n == TicketTypeRepr::V1MixnetEntry as u8 => {
                Ok(TicketTypeRepr::V1MixnetEntry.into())
            }
            n if n == TicketTypeRepr::V1MixnetExit as u8 => Ok(TicketTypeRepr::V1MixnetExit.into()),
            n if n == TicketTypeRepr::V1WireguardEntry as u8 => {
                Ok(TicketTypeRepr::V1WireguardEntry.into())
            }
            n if n == TicketTypeRepr::V1WireguardExit as u8 => {
                Ok(TicketTypeRepr::V1WireguardExit.into())
            }
            _ => Err(UnknownTicketType),
        }
    }
}

impl From<TicketType> for TicketTypeRepr {
    fn from(value: TicketType) -> Self {
        match value {
            TicketType::V1MixnetEntry => TicketTypeRepr::V1MixnetEntry,
            TicketType::V1MixnetExit => TicketTypeRepr::V1MixnetExit,
            TicketType::V1WireguardEntry => TicketTypeRepr::V1WireguardEntry,
            TicketType::V1WireguardExit => TicketTypeRepr::V1WireguardExit,
        }
    }
}

impl From<TicketTypeRepr> for TicketType {
    fn from(value: TicketTypeRepr) -> Self {
        match value {
            TicketTypeRepr::V1MixnetEntry => TicketType::V1MixnetEntry,
            TicketTypeRepr::V1MixnetExit => TicketType::V1MixnetExit,
            TicketTypeRepr::V1WireguardEntry => TicketType::V1WireguardEntry,
            TicketTypeRepr::V1WireguardExit => TicketType::V1WireguardExit,
        }
    }
}

#[derive(Clone)]
pub struct ClientTicket {
    pub spending_data: CredentialSpendingData,
    pub ticket_id: i64,
}

impl ClientTicket {
    pub fn new(spending_data: CredentialSpendingData, ticket_id: i64) -> Self {
        ClientTicket {
            spending_data,
            ticket_id,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct AvailableBandwidth {
    pub bytes: i64,
    pub expiration: OffsetDateTime,
}

impl AvailableBandwidth {
    pub fn expired(&self) -> bool {
        self.expiration < ecash_today()
    }
}

impl Default for AvailableBandwidth {
    fn default() -> Self {
        Self {
            bytes: 0,
            expiration: OffsetDateTime::UNIX_EPOCH,
        }
    }
}

#[derive(Debug, Copy, Clone)]
pub struct Bandwidth {
    value: u64,
}

impl Bandwidth {
    pub const fn new_unchecked(value: u64) -> Bandwidth {
        Bandwidth { value }
    }

    pub fn ticket_amount(typ: TicketTypeRepr) -> Self {
        Bandwidth {
            value: typ.bandwidth_value(),
        }
    }

    pub fn value(&self) -> u64 {
        self.value
    }
}