nym_credential_proxy_lib/deposits_buffer/
helpers.rs1use 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 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 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 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 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 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}