nym_credential_proxy_lib/deposits_buffer/
helpers.rs

1// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: GPL-3.0-only
3
4use crate::error::CredentialProxyError;
5use crate::shared_state::nyxd_client::ChainClient;
6use crate::storage::models::StorableEcashDeposit;
7use nym_compact_ecash::WithdrawalRequest;
8use nym_credentials::IssuanceTicketBook;
9use nym_crypto::asymmetric::ed25519;
10use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
11use nym_validator_client::nyxd::{Coin, Hash};
12use rand::rngs::OsRng;
13use std::fmt::Debug;
14use time::OffsetDateTime;
15use tokio_util::sync::CancellationToken;
16use tracing::{error, info, instrument};
17use zeroize::Zeroizing;
18
19pub struct BufferedDeposit {
20    pub deposit_id: u32,
21
22    // note: this type implements `ZeroizeOnDrop`
23    pub ed25519_private_key: ed25519::PrivateKey,
24}
25
26impl TryFrom<StorableEcashDeposit> for BufferedDeposit {
27    type Error = CredentialProxyError;
28
29    fn try_from(deposit: StorableEcashDeposit) -> Result<Self, Self::Error> {
30        let ed25519_private_key = ed25519::PrivateKey::from_bytes(
31            deposit.ed25519_deposit_private_key.as_ref(),
32        )
33        .map_err(|err| CredentialProxyError::DatabaseInconsistency {
34            reason: format!("one of the stored deposit ed25519 private keys is malformed: {err}"),
35        })?;
36
37        Ok(BufferedDeposit {
38            deposit_id: deposit.deposit_id,
39            ed25519_private_key,
40        })
41    }
42}
43
44impl BufferedDeposit {
45    pub fn new(deposit_id: u32, ed25519_private_key: ed25519::PrivateKey) -> Self {
46        BufferedDeposit {
47            deposit_id,
48            ed25519_private_key,
49        }
50    }
51
52    pub fn sign_ticketbook_plaintext(
53        &self,
54        withdrawal_request: &WithdrawalRequest,
55    ) -> ed25519::Signature {
56        let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id);
57        self.ed25519_private_key.sign(plaintext)
58    }
59}
60
61pub struct PerformedDeposits {
62    pub deposits_data: Vec<BufferedDeposit>,
63
64    // shared by all performed deposits as they were included in the same tx
65    pub tx_hash: Hash,
66    pub requested_on: OffsetDateTime,
67    pub deposit_amount: Coin,
68}
69
70impl PerformedDeposits {
71    pub(crate) fn to_storable(&self) -> Vec<StorableEcashDeposit> {
72        self.deposits_data
73            .iter()
74            .map(|d| StorableEcashDeposit {
75                deposit_id: d.deposit_id,
76                deposit_tx_hash: self.tx_hash.to_string(),
77                requested_on: self.requested_on,
78                deposit_amount: self.deposit_amount.to_string(),
79                ed25519_deposit_private_key: Zeroizing::new(d.ed25519_private_key.to_bytes()),
80            })
81            .collect()
82    }
83}
84
85#[instrument(skip(client, cancellation_on_critical_failure), err(Display))]
86pub async fn make_deposits_request(
87    client: &ChainClient,
88    deposit_amount: Coin,
89    memo: impl Into<String> + Debug,
90    amount: usize,
91    cancellation_on_critical_failure: &CancellationToken,
92) -> Result<PerformedDeposits, CredentialProxyError> {
93    let requested_on = OffsetDateTime::now_utc();
94    let chain_write_permit = client.start_chain_tx().await;
95    let mut rng = OsRng;
96
97    let keys = (0..amount)
98        .map(|_| ed25519::PrivateKey::new(&mut rng))
99        .collect::<Vec<_>>();
100
101    info!("starting {amount} deposits");
102    let mut contents = Vec::new();
103    for key in &keys {
104        let public_key: ed25519::PublicKey = key.into();
105        contents.push((public_key.to_base58_string(), deposit_amount.clone()));
106    }
107
108    let execute_res = chain_write_permit
109        .make_deposits(memo.into(), contents)
110        .await?;
111
112    let tx_hash = execute_res.transaction_hash;
113    info!("{amount} deposits made in transaction: {tx_hash}");
114
115    let contract_data = match execute_res.to_contract_data() {
116        Ok(contract_data) => contract_data,
117        Err(err) => {
118            // that one is tricky. deposits technically got made, but we somehow failed to parse response,
119            // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
120            // because it requires some serious MANUAL intervention
121            error!(
122                "CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"
123            );
124            cancellation_on_critical_failure.cancel();
125            return Err(CredentialProxyError::DepositFailure);
126        }
127    };
128
129    if contract_data.len() != amount {
130        // another critical failure, that one should be quite impossible and thus has to be manually inspected
131        error!(
132            "CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually",
133            contract_data.len()
134        );
135        cancellation_on_critical_failure.cancel();
136        return Err(CredentialProxyError::DepositFailure);
137    }
138
139    let mut deposits_data = Vec::new();
140    for (key, response) in keys.into_iter().zip(contract_data) {
141        let response_index = response.message_index;
142        let deposit_id = match response.parse_singleton_u32_contract_data() {
143            Ok(deposit_id) => deposit_id,
144            Err(err) => {
145                // another impossibility
146                error!(
147                    "CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"
148                );
149                cancellation_on_critical_failure.cancel();
150                return Err(CredentialProxyError::DepositFailure);
151            }
152        };
153
154        deposits_data.push(BufferedDeposit::new(deposit_id, key));
155    }
156
157    Ok(PerformedDeposits {
158        deposits_data,
159        tx_hash,
160        requested_on,
161        deposit_amount,
162    })
163}
164
165pub fn split_deposits(total: usize, max_request_size: usize) -> impl Iterator<Item = usize> {
166    (0..total)
167        .step_by(max_request_size)
168        .map(move |start| std::cmp::min(max_request_size, total - start))
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn request_sizes_test() {
177        assert_eq!(
178            split_deposits(100, 32).collect::<Vec<_>>(),
179            vec![32, 32, 32, 4]
180        );
181
182        assert_eq!(split_deposits(10, 32).collect::<Vec<_>>(), vec![10]);
183        assert_eq!(split_deposits(32, 32).collect::<Vec<_>>(), vec![32]);
184        assert_eq!(split_deposits(33, 32).collect::<Vec<_>>(), vec![32, 1]);
185        assert_eq!(split_deposits(1, 32).collect::<Vec<_>>(), vec![1]);
186    }
187}