Skip to main content

nym_credentials_interface/
lib.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use rand::Rng;
5use serde::{Deserialize, Serialize};
6use std::fmt::Debug;
7use strum::IntoEnumIterator as _;
8use thiserror::Error;
9use time::{Date, OffsetDateTime};
10
11pub use nym_compact_ecash::{
12    Base58, BlindedSignature, Bytable, EncodedDate, EncodedTicketType, PartialWallet, PayInfo,
13    PublicKeyUser, SecretKeyUser, VerificationKeyAuth, WithdrawalRequest,
14    aggregate_verification_keys, aggregate_wallets, constants, ecash_parameters,
15    error::CompactEcashError,
16    generate_keypair_user, generate_keypair_user_from_seed, issue_verify,
17    scheme::Payment,
18    scheme::coin_indices_signatures::aggregate_indices_signatures,
19    scheme::coin_indices_signatures::{
20        AnnotatedCoinIndexSignature, CoinIndexSignature, CoinIndexSignatureShare,
21        PartialCoinIndexSignature,
22    },
23    scheme::expiration_date_signatures::aggregate_expiration_signatures,
24    scheme::expiration_date_signatures::{
25        AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare,
26        PartialExpirationDateSignature,
27    },
28    scheme::keygen::KeyPairUser,
29    scheme::withdrawal::RequestInfo,
30    scheme::{Wallet, WalletSignatures},
31    withdrawal_request,
32};
33pub use nym_ecash_time::{EcashTime, ecash_today};
34pub use nym_network_defaults::TicketTypeRepr;
35use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
36
37/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms
38/// to increase their allowance.
39// currently defined as 20% of entry ticket value
40// clients are, of course, free to override this value
41pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 =
42    (V1MixnetEntry.bandwidth_value() / 5) as i64;
43
44#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
45pub enum BandwidthCredential {
46    ZkNym(Box<CredentialSpendingData>),
47    UpgradeModeJWT { token: String },
48}
49
50impl BandwidthCredential {
51    pub fn into_zk_nym(self) -> Option<Box<CredentialSpendingData>> {
52        match self {
53            BandwidthCredential::ZkNym(credential) => Some(credential),
54            _ => None,
55        }
56    }
57}
58
59impl From<CredentialSpendingData> for BandwidthCredential {
60    fn from(credential: CredentialSpendingData) -> Self {
61        Self::ZkNym(Box::new(credential))
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct CredentialSigningData {
67    pub withdrawal_request: WithdrawalRequest,
68
69    pub request_info: RequestInfo,
70
71    pub ecash_pub_key: PublicKeyUser,
72
73    pub expiration_date: Date,
74
75    pub ticketbook_type: TicketType,
76}
77
78#[derive(Serialize, Deserialize, PartialEq, Clone)]
79pub struct CredentialSpendingData {
80    pub payment: Payment,
81
82    pub pay_info: PayInfo,
83
84    pub spend_date: Date,
85
86    // pub value: u64,
87    /// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation.
88    pub epoch_id: u64,
89}
90
91impl Debug for CredentialSpendingData {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        // we're redacting the payment not since it contains secret,
94        // but because it's producing a lot of noise in the output and
95        // we are not really interested in coordinates of each of the attached curve points
96        f.debug_struct("CredentialSpendingData")
97            .field("payment", &"[REDACTED]")
98            .field("pay_info", &self.pay_info)
99            .field("spend_date", &self.spend_date)
100            .field("epoch_id", &self.epoch_id)
101            .finish()
102    }
103}
104
105impl CredentialSpendingData {
106    pub fn verify(&self, verification_key: &VerificationKeyAuth) -> Result<(), CompactEcashError> {
107        self.payment.spend_verify(
108            verification_key,
109            &self.pay_info,
110            self.spend_date.ecash_unix_timestamp(),
111        )
112    }
113
114    pub fn encoded_serial_number(&self) -> Vec<u8> {
115        self.payment.encoded_serial_number()
116    }
117
118    pub fn serial_number_b58(&self) -> String {
119        self.payment.serial_number_bs58()
120    }
121
122    pub fn to_bytes(&self) -> Vec<u8> {
123        // simple length prefixed serialization
124        // TODO: change it to a standard format instead
125        let mut bytes = Vec::new();
126        let payment_bytes = self.payment.to_bytes();
127
128        bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes());
129        bytes.extend_from_slice(&payment_bytes);
130        bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long
131        bytes.extend_from_slice(&self.spend_date.to_julian_day().to_be_bytes());
132        bytes.extend_from_slice(&self.epoch_id.to_be_bytes());
133
134        bytes
135    }
136
137    pub fn try_from_bytes(raw: &[u8]) -> Result<Self, CompactEcashError> {
138        // minimum length: 72 (pay_info) + 8 (epoch_id) + 4 (spend date) + 4 (payment length prefix)
139        if raw.len() < 72 + 8 + 4 + 4 {
140            return Err(CompactEcashError::DeserializationFailure {
141                object: "EcashCredential".into(),
142            });
143        }
144        let mut index = 0;
145        //SAFETY : casting a slice of length 4 into an array of size 4
146        let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize;
147        index += 4;
148
149        if raw[index..].len() != payment_len + 84 {
150            return Err(CompactEcashError::DeserializationFailure {
151                object: "EcashCredential".into(),
152            });
153        }
154        let payment = Payment::try_from(&raw[index..index + payment_len])?;
155        index += payment_len;
156
157        let pay_info = PayInfo {
158            //SAFETY : casting a slice of length 72 into an array of size 72
159            pay_info_bytes: raw[index..index + 72].try_into().unwrap(),
160        };
161        index += 72;
162
163        //SAFETY : casting a slice of length 4 into an array of size 4
164        let spend_date_julian = i32::from_be_bytes(raw[index..index + 4].try_into().unwrap());
165        let spend_date = Date::from_julian_day(spend_date_julian).map_err(|_| {
166            CompactEcashError::DeserializationFailure {
167                object: "CredentialSpendingData".into(),
168            }
169        })?;
170        index += 4;
171
172        if raw[index..].len() != 8 {
173            return Err(CompactEcashError::DeserializationFailure {
174                object: "EcashCredential".into(),
175            });
176        }
177
178        //SAFETY : casting a slice of length 8 into an array of size 8
179        let epoch_id = u64::from_be_bytes(raw[index..].try_into().unwrap());
180
181        Ok(CredentialSpendingData {
182            payment,
183            pay_info,
184            spend_date,
185            epoch_id,
186        })
187    }
188}
189
190impl Bytable for CredentialSpendingData {
191    fn to_byte_vec(&self) -> Vec<u8> {
192        self.to_bytes()
193    }
194
195    fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
196        Self::try_from_bytes(slice)
197    }
198}
199
200impl Base58 for CredentialSpendingData {}
201
202#[derive(PartialEq, Eq, Debug, Clone, Copy)]
203pub struct NymPayInfo {
204    randomness: [u8; 32],
205    timestamp: i64,
206    provider_public_key: [u8; 32],
207}
208
209impl NymPayInfo {
210    /// Generates a new `NymPayInfo` instance with random bytes, a timestamp, and a provider public key.
211    ///
212    /// # Arguments
213    ///
214    /// * `provider_pk` - The public key of the payment provider.
215    ///
216    /// # Returns
217    ///
218    /// A new `NymPayInfo` instance.
219    ///
220    pub fn generate(provider_pk: [u8; 32], spend_time: OffsetDateTime) -> Self {
221        let mut randomness = [0u8; 32];
222        rand::thread_rng().fill(&mut randomness[..32]);
223
224        let timestamp = spend_time.unix_timestamp();
225
226        NymPayInfo {
227            randomness,
228            timestamp,
229            provider_public_key: provider_pk,
230        }
231    }
232
233    pub fn timestamp(&self) -> i64 {
234        self.timestamp
235    }
236
237    pub fn pk(&self) -> [u8; 32] {
238        self.provider_public_key
239    }
240}
241
242impl From<NymPayInfo> for PayInfo {
243    fn from(value: NymPayInfo) -> Self {
244        let mut pay_info_bytes = [0u8; 72];
245
246        pay_info_bytes[..32].copy_from_slice(&value.randomness);
247        pay_info_bytes[32..40].copy_from_slice(&value.timestamp.to_be_bytes());
248        pay_info_bytes[40..].copy_from_slice(&value.provider_public_key);
249
250        PayInfo { pay_info_bytes }
251    }
252}
253
254impl From<PayInfo> for NymPayInfo {
255    fn from(value: PayInfo) -> Self {
256        //SAFETY : slice to array of same length
257        let randomness = value.pay_info_bytes[..32].try_into().unwrap();
258        let timestamp = i64::from_be_bytes(value.pay_info_bytes[32..40].try_into().unwrap());
259        let provider_public_key = value.pay_info_bytes[40..].try_into().unwrap();
260
261        NymPayInfo {
262            randomness,
263            timestamp,
264            provider_public_key,
265        }
266    }
267}
268
269#[derive(
270    Copy,
271    Clone,
272    Debug,
273    PartialEq,
274    Eq,
275    Serialize,
276    Deserialize,
277    Hash,
278    strum_macros::Display,
279    strum_macros::EnumString,
280    strum_macros::EnumIter,
281)]
282#[serde(rename_all = "kebab-case")]
283#[strum(serialize_all = "kebab-case")]
284pub enum TicketType {
285    V1MixnetEntry,
286    V1MixnetExit,
287    V1WireguardEntry,
288    V1WireguardExit,
289}
290
291#[derive(Debug, Copy, Clone, Error)]
292#[error("provided unknown ticketbook type")]
293pub struct UnknownTicketType;
294
295impl TicketType {
296    pub fn to_repr(&self) -> TicketTypeRepr {
297        (*self).into()
298    }
299
300    pub fn encode(&self) -> EncodedTicketType {
301        self.to_repr() as EncodedTicketType
302    }
303
304    pub fn try_from_encoded(val: EncodedTicketType) -> Result<Self, UnknownTicketType> {
305        match val {
306            n if n == TicketTypeRepr::V1MixnetEntry as u8 => {
307                Ok(TicketTypeRepr::V1MixnetEntry.into())
308            }
309            n if n == TicketTypeRepr::V1MixnetExit as u8 => Ok(TicketTypeRepr::V1MixnetExit.into()),
310            n if n == TicketTypeRepr::V1WireguardEntry as u8 => {
311                Ok(TicketTypeRepr::V1WireguardEntry.into())
312            }
313            n if n == TicketTypeRepr::V1WireguardExit as u8 => {
314                Ok(TicketTypeRepr::V1WireguardExit.into())
315            }
316            _ => Err(UnknownTicketType),
317        }
318    }
319
320    pub fn exposed_iter() -> impl Iterator<Item = TicketType> {
321        TicketType::iter()
322    }
323}
324
325impl From<TicketType> for TicketTypeRepr {
326    fn from(value: TicketType) -> Self {
327        match value {
328            TicketType::V1MixnetEntry => TicketTypeRepr::V1MixnetEntry,
329            TicketType::V1MixnetExit => TicketTypeRepr::V1MixnetExit,
330            TicketType::V1WireguardEntry => TicketTypeRepr::V1WireguardEntry,
331            TicketType::V1WireguardExit => TicketTypeRepr::V1WireguardExit,
332        }
333    }
334}
335
336impl From<TicketTypeRepr> for TicketType {
337    fn from(value: TicketTypeRepr) -> Self {
338        match value {
339            TicketTypeRepr::V1MixnetEntry => TicketType::V1MixnetEntry,
340            TicketTypeRepr::V1MixnetExit => TicketType::V1MixnetExit,
341            TicketTypeRepr::V1WireguardEntry => TicketType::V1WireguardEntry,
342            TicketTypeRepr::V1WireguardExit => TicketType::V1WireguardExit,
343        }
344    }
345}
346
347#[derive(Clone)]
348pub struct ClientTicket {
349    pub spending_data: CredentialSpendingData,
350    pub ticket_id: i64,
351}
352
353impl ClientTicket {
354    pub fn new(spending_data: CredentialSpendingData, ticket_id: i64) -> Self {
355        ClientTicket {
356            spending_data,
357            ticket_id,
358        }
359    }
360}
361
362#[derive(Debug, Clone, Copy)]
363pub struct AvailableBandwidth {
364    pub bytes: i64,
365    pub expiration: OffsetDateTime,
366}
367
368impl AvailableBandwidth {
369    pub fn expired(&self) -> bool {
370        self.expiration < ecash_today()
371    }
372}
373
374impl Default for AvailableBandwidth {
375    fn default() -> Self {
376        Self {
377            bytes: 0,
378            expiration: OffsetDateTime::UNIX_EPOCH,
379        }
380    }
381}
382
383#[derive(Debug, Copy, Clone)]
384pub struct Bandwidth {
385    value: u64,
386}
387
388impl Bandwidth {
389    pub const fn new_unchecked(value: u64) -> Bandwidth {
390        Bandwidth { value }
391    }
392
393    pub fn ticket_amount(typ: TicketTypeRepr) -> Self {
394        Bandwidth {
395            value: typ.bandwidth_value(),
396        }
397    }
398
399    pub fn value(&self) -> u64 {
400        self.value
401    }
402}