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