libzeropool_rs/client/
mod.rs

1use std::{convert::TryInto, io::Write};
2
3use kvdb::KeyValueDB;
4use libzeropool::{
5    constants,
6    fawkes_crypto::{
7        core::sizedvec::SizedVec,
8        ff_uint::{Num, NumRepr, PrimeField, Uint},
9        rand::Rng,
10    },
11    native::{
12        account::Account,
13        boundednum::BoundedNum,
14        cipher,
15        key::derive_key_p_d,
16        note::Note,
17        params::PoolParams,
18        tx::{
19            make_delta, nullifier, out_commitment_hash, tx_hash, tx_sign, TransferPub, TransferSec,
20            Tx,
21        },
22    },
23};
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27use self::state::{State, Transaction};
28use crate::{
29    address::{format_address, parse_address, AddressParseError},
30    keys::{reduce_sk, Keys},
31    merkle::Hash,
32    random::CustomRng,
33    utils::{keccak256, zero_note, zero_proof},
34};
35
36pub mod state;
37
38#[derive(Debug, Error)]
39pub enum CreateTxError {
40    #[error("Too many outputs: expected {max} max got {got}")]
41    TooManyOutputs { max: usize, got: usize },
42    #[error("Could not get merkle proof for leaf {0}")]
43    ProofNotFound(u64),
44    #[error("Failed to parse address: {0}")]
45    AddressParseError(#[from] AddressParseError),
46    #[error("Insufficient balance: sum of outputs is greater than sum of inputs: {0} > {1}")]
47    InsufficientBalance(String, String),
48    #[error("Insufficient energy: available {0}, received {1}")]
49    InsufficientEnergy(String, String),
50}
51
52#[derive(Serialize, Deserialize, Default, Clone)]
53pub struct StateFragment<Fr: PrimeField> {
54    pub new_leafs: Vec<(u64, Vec<Hash<Fr>>)>,
55    pub new_commitments: Vec<(u64, Hash<Fr>)>,
56    pub new_accounts: Vec<(u64, Account<Fr>)>,
57    pub new_notes: Vec<(u64, Note<Fr>)>,
58}
59
60#[derive(Clone, Serialize, Deserialize)]
61pub struct TransactionData<Fr: PrimeField> {
62    pub public: TransferPub<Fr>,
63    pub secret: TransferSec<Fr>,
64    pub ciphertext: Vec<u8>,
65    pub memo: Vec<u8>,
66    pub commitment_root: Num<Fr>,
67    pub out_hashes: SizedVec<Num<Fr>, { constants::OUT + 1 }>,
68}
69
70pub type TokenAmount<Fr> = BoundedNum<Fr, { constants::BALANCE_SIZE_BITS }>;
71
72#[derive(Debug, Serialize, Deserialize, Clone)]
73pub struct TxOutput<Fr: PrimeField> {
74    pub to: String,
75    pub amount: TokenAmount<Fr>,
76}
77
78#[derive(Debug, Serialize, Deserialize, Clone)]
79pub enum TxType<Fr: PrimeField> {
80    Transfer {
81        fee: TokenAmount<Fr>,
82        outputs: Vec<TxOutput<Fr>>,
83    },
84    Deposit {
85        fee: TokenAmount<Fr>,
86        deposit_amount: TokenAmount<Fr>,
87        outputs: Vec<TxOutput<Fr>>,
88    },
89    DepositPermittable {
90        fee: TokenAmount<Fr>,
91        deposit_amount: TokenAmount<Fr>,
92        deadline: u64,
93        holder: Vec<u8>,
94        outputs: Vec<TxOutput<Fr>>,
95    },
96    Withdraw {
97        fee: TokenAmount<Fr>,
98        withdraw_amount: TokenAmount<Fr>,
99        to: Vec<u8>,
100        native_amount: TokenAmount<Fr>,
101        energy_amount: TokenAmount<Fr>,
102    },
103}
104
105pub struct UserAccount<D: KeyValueDB, P: PoolParams> {
106    pub pool_id: BoundedNum<P::Fr, { constants::DIVERSIFIER_SIZE_BITS }>,
107    pub keys: Keys<P>,
108    pub params: P,
109    pub state: State<D, P>,
110    pub sign_callback: Option<Box<dyn Fn(&[u8]) -> Vec<u8>>>, // TODO: Find a way to make it async
111}
112
113impl<'p, D, P> UserAccount<D, P>
114where
115    D: KeyValueDB,
116    P: PoolParams,
117    P::Fr: 'static,
118{
119    /// Initializes UserAccount with a spending key that has to be an element of the prime field Fs (p = 6554484396890773809930967563523245729705921265872317281365359162392183254199).
120    pub fn new(sk: Num<P::Fs>, state: State<D, P>, params: P) -> Self {
121        let keys = Keys::derive(sk, &params);
122
123        UserAccount {
124            // For now it is constant, but later should be provided by user
125            pool_id: BoundedNum::new(Num::ZERO),
126            keys,
127            state,
128            params,
129            sign_callback: None,
130        }
131    }
132
133    /// Same as constructor but accepts arbitrary data as spending key.
134    pub fn from_seed(seed: &[u8], state: State<D, P>, params: P) -> Self {
135        let sk = reduce_sk(seed);
136        Self::new(sk, state, params)
137    }
138
139    fn generate_address_components(
140        &self,
141    ) -> (
142        BoundedNum<P::Fr, { constants::DIVERSIFIER_SIZE_BITS }>,
143        Num<P::Fr>,
144    ) {
145        let mut rng = CustomRng;
146
147        let d: BoundedNum<_, { constants::DIVERSIFIER_SIZE_BITS }> = rng.gen();
148        let pk_d = derive_key_p_d(d.to_num(), self.keys.eta, &self.params);
149        (d, pk_d.x)
150    }
151
152    /// Generates a new private address.
153    pub fn generate_address(&self) -> String {
154        let (d, p_d) = self.generate_address_components();
155
156        format_address::<P>(d, p_d)
157    }
158
159    /// Attempts to decrypt notes.
160    pub fn decrypt_notes(&self, data: Vec<u8>) -> Vec<Option<Note<P::Fr>>> {
161        cipher::decrypt_in(self.keys.eta, &data, &self.params)
162    }
163
164    /// Attempts to decrypt account and notes.
165    pub fn decrypt_pair(&self, data: Vec<u8>) -> Option<(Account<P::Fr>, Vec<Note<P::Fr>>)> {
166        cipher::decrypt_out(self.keys.eta, &data, &self.params)
167    }
168
169    pub fn is_own_address(&self, address: &str) -> bool {
170        let mut result = false;
171        if let Ok((d, p_d)) = parse_address::<P>(address) {
172            let own_p_d = derive_key_p_d(d.to_num(), self.keys.eta, &self.params).x;
173            result = own_p_d == p_d;
174        }
175
176        result
177    }
178
179    /// Constructs a transaction.
180    pub fn create_tx(
181        &self,
182        tx: TxType<P::Fr>,
183        delta_index: Option<u64>,
184        extra_state: Option<StateFragment<P::Fr>>,
185    ) -> Result<TransactionData<P::Fr>, CreateTxError> {
186        let mut rng = CustomRng;
187        let keys = self.keys.clone();
188        let state = &self.state;
189
190        let extra_state = extra_state.unwrap_or(StateFragment {
191            new_leafs: [].to_vec(),
192            new_commitments: [].to_vec(),
193            new_accounts: [].to_vec(),
194            new_notes: [].to_vec(),
195        });
196
197        // initial input account (from optimistic state)
198        let (in_account_optimistic_index, in_account_optimistic) = {
199            let last_acc = extra_state.new_accounts.last();
200            match last_acc {
201                Some(last_acc) => (Some(last_acc.0), Some(last_acc.1)),
202                _ => (None, None),
203            }
204        };
205
206        // initial input account (from non-optimistic state)
207        let in_account = in_account_optimistic.unwrap_or_else(|| {
208            state.latest_account.unwrap_or_else(|| {
209                // Initial account should have d = pool_id to protect from reply attacks
210                let d = self.pool_id;
211                let p_d = derive_key_p_d(d.to_num(), self.keys.eta, &self.params).x;
212                Account {
213                    d: self.pool_id,
214                    p_d,
215                    i: BoundedNum::new(Num::ZERO),
216                    b: BoundedNum::new(Num::ZERO),
217                    e: BoundedNum::new(Num::ZERO),
218                }
219            })
220        });
221
222        let tree = &self.state.tree;
223
224        let in_account_index = in_account_optimistic_index.or(state.latest_account_index);
225
226        // initial usable note index
227        let next_usable_index = state
228            .earliest_usable_index_optimistic(&extra_state.new_accounts, &extra_state.new_notes);
229
230        let latest_note_index_optimistic = extra_state
231            .new_notes
232            .last()
233            .map(|indexed_note| indexed_note.0)
234            .unwrap_or(state.latest_note_index);
235
236        // Should be provided by relayer together with note proofs, but as a fallback
237        // take the next index of the tree (optimistic part included).
238        let delta_index = Num::from(delta_index.unwrap_or_else(|| {
239            let next_by_optimistic_leaf = extra_state.new_leafs.last().map(|leafs| {
240                (((leafs.0 + (leafs.1.len() as u64)) >> constants::OUTPLUSONELOG) + 1)
241                    << constants::OUTPLUSONELOG
242            });
243            let next_by_optimistic_commitment =
244                extra_state.new_commitments.last().map(|commitment| {
245                    ((commitment.0 >> constants::OUTPLUSONELOG) + 1) << constants::OUTPLUSONELOG
246                });
247            next_by_optimistic_leaf
248                .into_iter()
249                .chain(next_by_optimistic_commitment)
250                .max()
251                .unwrap_or(self.state.tree.next_index())
252        }));
253
254        let (fee, tx_data) = {
255            let mut tx_data: Vec<u8> = vec![];
256            match &tx {
257                TxType::Deposit { fee, .. } => {
258                    let raw_fee: u64 = fee.to_num().try_into().unwrap();
259                    tx_data.write_all(&raw_fee.to_be_bytes()).unwrap();
260                    (fee, tx_data)
261                }
262                TxType::DepositPermittable {
263                    fee,
264                    deadline,
265                    holder,
266                    ..
267                } => {
268                    let raw_fee: u64 = fee.to_num().try_into().unwrap();
269
270                    tx_data.write_all(&raw_fee.to_be_bytes()).unwrap();
271                    tx_data.write_all(&deadline.to_be_bytes()).unwrap();
272                    tx_data.append(&mut holder.clone());
273
274                    (fee, tx_data)
275                }
276                TxType::Transfer { fee, .. } => {
277                    let raw_fee: u64 = fee.to_num().try_into().unwrap();
278                    tx_data.write_all(&raw_fee.to_be_bytes()).unwrap();
279                    (fee, tx_data)
280                }
281                TxType::Withdraw {
282                    fee,
283                    to,
284                    native_amount,
285                    ..
286                } => {
287                    let raw_fee: u64 = fee.to_num().try_into().unwrap();
288                    let raw_native_amount: u64 = native_amount.to_num().try_into().unwrap();
289
290                    tx_data.write_all(&raw_fee.to_be_bytes()).unwrap();
291                    tx_data.write_all(&raw_native_amount.to_be_bytes()).unwrap();
292                    tx_data.append(&mut to.clone());
293
294                    (fee, tx_data)
295                }
296            }
297        };
298
299        // Optimistic available notes
300        let optimistic_available_notes = extra_state
301            .new_notes
302            .into_iter()
303            .filter(|indexed_note| indexed_note.0 >= next_usable_index);
304
305        // Fetch constants::IN usable notes from state
306        let in_notes_original: Vec<(u64, Note<P::Fr>)> = state
307            .txs
308            .iter_slice(next_usable_index..=state.latest_note_index)
309            .filter_map(|(index, tx)| match tx {
310                Transaction::Note(note) => Some((index, note)),
311                _ => None,
312            })
313            .chain(optimistic_available_notes)
314            .take(constants::IN)
315            .collect();
316
317        let spend_interval_index = in_notes_original
318            .last()
319            .map(|(index, _)| *index + 1)
320            .unwrap_or(if latest_note_index_optimistic > 0 {
321                latest_note_index_optimistic + 1
322            } else {
323                0
324            });
325
326        // Calculate total balance (account + constants::IN notes).
327        let mut input_value = in_account.b.to_num();
328        for (_index, note) in &in_notes_original {
329            input_value += note.b.to_num();
330        }
331
332        let mut output_value = Num::ZERO;
333
334        let (num_real_out_notes, out_notes) = match &tx {
335            TxType::Transfer { outputs, .. }
336            | TxType::Deposit { outputs, .. }
337            | TxType::DepositPermittable { outputs, .. } => {
338                if outputs.len() >= constants::OUT {
339                    return Err(CreateTxError::TooManyOutputs {
340                        max: constants::OUT,
341                        got: outputs.len(),
342                    });
343                }
344
345                let out_notes = outputs
346                    .iter()
347                    .map(|dest| {
348                        let (to_d, to_p_d) = parse_address::<P>(&dest.to)?;
349
350                        output_value += dest.amount.to_num();
351
352                        Ok(Note {
353                            d: to_d,
354                            p_d: to_p_d,
355                            b: dest.amount,
356                            t: rng.gen(),
357                        })
358                    })
359                    // fill out remaining output notes with zeroes
360                    .chain((0..).map(|_| Ok(zero_note())))
361                    .take(constants::OUT)
362                    .collect::<Result<SizedVec<_, { constants::OUT }>, AddressParseError>>()?;
363
364                (outputs.len(), out_notes)
365            }
366            _ => (0, (0..).map(|_| zero_note()).take(constants::OUT).collect()),
367        };
368
369        let mut delta_value = -fee.as_num();
370        // By default all account energy will be withdrawn on withdraw tx
371        let mut delta_energy = Num::ZERO;
372
373        let in_account_pos = in_account_index.unwrap_or(0);
374
375        let mut input_energy = in_account.e.to_num();
376        input_energy += in_account.b.to_num() * (delta_index - Num::from(in_account_pos));
377
378        for (note_index, note) in &in_notes_original {
379            input_energy += note.b.to_num() * (delta_index - Num::from(*note_index));
380        }
381        let new_balance = match &tx {
382            TxType::Transfer { .. } => {
383                if input_value.to_uint() >= (output_value + fee.as_num()).to_uint() {
384                    input_value - output_value - fee.as_num()
385                } else {
386                    return Err(CreateTxError::InsufficientBalance(
387                        (output_value + fee.as_num()).to_string(),
388                        input_value.to_string(),
389                    ));
390                }
391            }
392            TxType::Withdraw {
393                withdraw_amount,
394                energy_amount,
395                ..
396            } => {
397                let amount = withdraw_amount.to_num();
398                let energy = energy_amount.to_num();
399
400                if energy.to_uint() > input_energy.to_uint() {
401                    return Err(CreateTxError::InsufficientEnergy(
402                        input_energy.to_string(),
403                        energy.to_string(),
404                    ));
405                }
406
407                delta_energy -= energy;
408                delta_value -= amount;
409
410                if input_value.to_uint() >= amount.to_uint() {
411                    input_value + delta_value
412                } else {
413                    return Err(CreateTxError::InsufficientBalance(
414                        delta_value.to_string(),
415                        input_value.to_string(),
416                    ));
417                }
418            }
419            TxType::Deposit { deposit_amount, .. }
420            | TxType::DepositPermittable { deposit_amount, .. } => {
421                delta_value += deposit_amount.to_num();
422                let new_total_balance = input_value + delta_value;
423                if new_total_balance.to_uint() >= output_value.to_uint() {
424                    new_total_balance - output_value
425                } else {
426                    return Err(CreateTxError::InsufficientBalance(
427                        output_value.to_string(),
428                        new_total_balance.to_string(),
429                    ));
430                }
431            }
432        };
433
434        let (d, p_d) = self.generate_address_components();
435        let out_account = Account {
436            d,
437            p_d,
438            i: BoundedNum::new(Num::from(spend_interval_index)),
439            b: BoundedNum::new(new_balance),
440            e: BoundedNum::new(delta_energy + input_energy),
441        };
442
443        let in_account_hash = in_account.hash(&self.params);
444        let nullifier = nullifier(
445            in_account_hash,
446            keys.eta,
447            in_account_pos.into(),
448            &self.params,
449        );
450
451        let ciphertext = {
452            let entropy: [u8; 32] = rng.gen();
453
454            // No need to include all the zero notes in the encrypted transaction
455            let out_notes = &out_notes[0..num_real_out_notes];
456
457            cipher::encrypt(&entropy, keys.eta, out_account, out_notes, &self.params)
458        };
459
460        // Hash input account + notes filling remaining space with non-hashed zeroes
461        let owned_zero_notes = (0..).map(|_| {
462            let d: BoundedNum<_, { constants::DIVERSIFIER_SIZE_BITS }> = rng.gen();
463            let p_d = derive_key_p_d::<P, P::Fr>(d.to_num(), keys.eta, &self.params).x;
464            Note {
465                d,
466                p_d,
467                b: BoundedNum::new(Num::ZERO),
468                t: rng.gen(),
469            }
470        });
471        let in_notes: SizedVec<Note<P::Fr>, { constants::IN }> = in_notes_original
472            .iter()
473            .map(|(_, note)| note)
474            .cloned()
475            .chain(owned_zero_notes)
476            .take(constants::IN)
477            .collect();
478        let in_note_hashes = in_notes.iter().map(|note| note.hash(&self.params));
479        let input_hashes: SizedVec<_, { constants::IN + 1 }> = [in_account_hash]
480            .iter()
481            .copied()
482            .chain(in_note_hashes)
483            .collect();
484
485        // Same with output
486        let out_account_hash = out_account.hash(&self.params);
487        let out_note_hashes = out_notes.iter().map(|n| n.hash(&self.params));
488        let out_hashes: SizedVec<Num<P::Fr>, { constants::OUT + 1 }> = [out_account_hash]
489            .iter()
490            .copied()
491            .chain(out_note_hashes)
492            .collect();
493
494        let out_commit = out_commitment_hash(out_hashes.as_slice(), &self.params);
495        let tx_hash = tx_hash(input_hashes.as_slice(), out_commit, &self.params);
496
497        let delta = make_delta::<P::Fr>(
498            delta_value,
499            delta_energy,
500            delta_index,
501            *self.pool_id.clone().as_num(),
502        );
503
504        // calculate virtual subtree from the optimistic state
505        let new_leafs = extra_state.new_leafs.iter().cloned();
506        let new_commitments = extra_state.new_commitments.iter().cloned();
507        let (mut virtual_nodes, update_boundaries) =
508            tree.get_virtual_subtree(new_leafs, new_commitments);
509
510        let root: Num<P::Fr> = tree.get_root_optimistic(&mut virtual_nodes, &update_boundaries);
511
512        // memo = tx_specific_data, ciphertext, user_defined_data
513        let mut memo_data = {
514            let tx_data_size = tx_data.len();
515            let ciphertext_size = ciphertext.len();
516            Vec::with_capacity(tx_data_size + ciphertext_size)
517        };
518
519        memo_data.extend(&tx_data);
520        memo_data.extend(&ciphertext);
521
522        let memo_hash = keccak256(&memo_data);
523        let memo = Num::from_uint_reduced(NumRepr(Uint::from_big_endian(&memo_hash)));
524
525        let public = TransferPub::<P::Fr> {
526            root,
527            nullifier,
528            out_commit,
529            delta,
530            memo,
531        };
532
533        let tx = Tx {
534            input: (in_account, in_notes),
535            output: (out_account, out_notes),
536        };
537
538        let (eddsa_s, eddsa_r) = tx_sign(keys.sk, tx_hash, &self.params);
539
540        let account_proof = in_account_index.map_or_else(
541            || Ok(zero_proof()),
542            |i| {
543                tree.get_proof_optimistic_index(i, &mut virtual_nodes, &update_boundaries)
544                    .ok_or(CreateTxError::ProofNotFound(i))
545            },
546        )?;
547        let note_proofs = in_notes_original
548            .iter()
549            .copied()
550            .map(|(index, _note)| {
551                tree.get_proof_optimistic_index(index, &mut virtual_nodes, &update_boundaries)
552                    .ok_or(CreateTxError::ProofNotFound(index))
553            })
554            .chain((0..).map(|_| Ok(zero_proof())))
555            .take(constants::IN)
556            .collect::<Result<_, _>>()?;
557
558        let secret = TransferSec::<P::Fr> {
559            tx,
560            in_proof: (account_proof, note_proofs),
561            eddsa_s: eddsa_s.to_other().unwrap(),
562            eddsa_r,
563            eddsa_a: keys.a,
564        };
565
566        Ok(TransactionData {
567            public,
568            secret,
569            ciphertext,
570            memo: memo_data,
571            commitment_root: out_commit,
572            out_hashes,
573        })
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use libzeropool::POOL_PARAMS;
580
581    use super::*;
582
583    #[test]
584    fn test_create_tx_deposit_zero() {
585        let state = State::init_test(POOL_PARAMS.clone());
586        let acc = UserAccount::new(Num::ZERO, state, POOL_PARAMS.clone());
587
588        acc.create_tx(
589            TxType::Deposit {
590                fee: BoundedNum::new(Num::ZERO),
591                deposit_amount: BoundedNum::new(Num::ZERO),
592                outputs: vec![],
593            },
594            None,
595            None,
596        )
597        .unwrap();
598    }
599
600    #[test]
601    fn test_create_tx_deposit_one() {
602        let state = State::init_test(POOL_PARAMS.clone());
603        let acc = UserAccount::new(Num::ZERO, state, POOL_PARAMS.clone());
604
605        acc.create_tx(
606            TxType::Deposit {
607                fee: BoundedNum::new(Num::ZERO),
608                deposit_amount: BoundedNum::new(Num::ONE),
609                outputs: vec![],
610            },
611            None,
612            None,
613        )
614        .unwrap();
615    }
616
617    // It's ok to transfer 0 while balance = 0
618    #[test]
619    fn test_create_tx_transfer_zero() {
620        let state = State::init_test(POOL_PARAMS.clone());
621        let acc = UserAccount::new(Num::ZERO, state, POOL_PARAMS.clone());
622
623        let addr = acc.generate_address();
624
625        let out = TxOutput {
626            to: addr,
627            amount: BoundedNum::new(Num::ZERO),
628        };
629
630        acc.create_tx(
631            TxType::Transfer {
632                fee: BoundedNum::new(Num::ZERO),
633                outputs: vec![out],
634            },
635            None,
636            None,
637        )
638        .unwrap();
639    }
640
641    #[test]
642    #[should_panic]
643    fn test_create_tx_transfer_one_no_balance() {
644        let state = State::init_test(POOL_PARAMS.clone());
645        let acc = UserAccount::new(Num::ZERO, state, POOL_PARAMS.clone());
646
647        let addr = acc.generate_address();
648
649        let out = TxOutput {
650            to: addr,
651            amount: BoundedNum::new(Num::ONE),
652        };
653
654        acc.create_tx(
655            TxType::Transfer {
656                fee: BoundedNum::new(Num::ZERO),
657                outputs: vec![out],
658            },
659            None,
660            None,
661        )
662        .unwrap();
663    }
664
665    #[test]
666    fn test_user_account_is_own_address() {
667        let acc_1 = UserAccount::new(
668            Num::ZERO,
669            State::init_test(POOL_PARAMS.clone()),
670            POOL_PARAMS.clone(),
671        );
672        let acc_2 = UserAccount::new(
673            Num::ONE,
674            State::init_test(POOL_PARAMS.clone()),
675            POOL_PARAMS.clone(),
676        );
677
678        let address_1 = acc_1.generate_address();
679        let address_2 = acc_2.generate_address();
680
681        assert!(acc_1.is_own_address(&address_1));
682        assert!(acc_2.is_own_address(&address_2));
683
684        assert!(!acc_1.is_own_address(&address_2));
685        assert!(!acc_2.is_own_address(&address_1));
686    }
687}