diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5381ccc34..2c5e58e5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - cron: '0 0 * * */2' env: CARGO_TERM_COLOR: always - RUSTFLAGS: -D warnings + RUSTFLAGS: -D warnings -A unknown-lints RUST_BACKTRACE: full jobs: test: diff --git a/manta-accounting/Cargo.toml b/manta-accounting/Cargo.toml index f8e338090..b8673365d 100644 --- a/manta-accounting/Cargo.toml +++ b/manta-accounting/Cargo.toml @@ -65,5 +65,5 @@ rayon = { version = "1.5.1", optional = true, default-features = false } statrs = { version = "0.15.0", optional = true, default-features = false } [dev-dependencies] -rand = "0.8.4" +manta-crypto = { path = "../manta-crypto", default-features = false, features = ["getrandom"] } diff --git a/manta-accounting/src/asset.rs b/manta-accounting/src/asset.rs index 4857ea4ac..171b050c3 100644 --- a/manta-accounting/src/asset.rs +++ b/manta-accounting/src/asset.rs @@ -1134,12 +1134,12 @@ where #[cfg(test)] mod test { use super::*; - use rand::thread_rng; + use manta_crypto::rand::OsRng; /// Tests asset conversion into and from bytes. #[test] fn asset_into_and_from_bytes() { - let mut rng = thread_rng(); + let mut rng = OsRng; let asset = Asset::gen(&mut rng); assert_eq!(asset, Asset::from_bytes(asset.into_bytes())); let mut asset_bytes = [0; Asset::SIZE]; @@ -1150,7 +1150,7 @@ mod test { /// Tests asset arithmetic. #[test] fn asset_arithmetic() { - let mut rng = thread_rng(); + let mut rng = OsRng; let mut asset = Asset::zero(rng.gen()); let value = rng.gen(); let _ = asset + value; diff --git a/manta-accounting/src/wallet/balance.rs b/manta-accounting/src/wallet/balance.rs new file mode 100644 index 000000000..8e7c13eda --- /dev/null +++ b/manta-accounting/src/wallet/balance.rs @@ -0,0 +1,226 @@ +// Copyright 2019-2022 Manta Network. +// This file is part of manta-rs. +// +// manta-rs is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// manta-rs is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with manta-rs. If not, see . + +//! Wallet Balance State +//! +//! This module defines the balance states of a wallet using the current private asset transfer +//! protocol. Applications which define balances beyond fungible assets should extend these +//! abstractions. + +use crate::asset::{Asset, AssetId, AssetList, AssetValue}; +use alloc::collections::btree_map::{BTreeMap, Entry as BTreeMapEntry}; + +#[cfg(feature = "std")] +use std::{ + collections::hash_map::{Entry as HashMapEntry, HashMap, RandomState}, + hash::BuildHasher, +}; + +/// Balance State +pub trait BalanceState: Default { + /// Returns the current balance associated with this `id`. + fn balance(&self, id: AssetId) -> AssetValue; + + /// Returns true if `self` contains at least `asset.value` of the asset of kind `asset.id`. + #[inline] + fn contains(&self, asset: Asset) -> bool { + self.balance(asset.id) >= asset.value + } + + /// Deposits `asset` into the balance state, increasing the balance of the asset stored at + /// `asset.id` by an amount equal to `asset.value`. + fn deposit(&mut self, asset: Asset); + + /// Deposits every asset in `assets` into the balance state. + #[inline] + fn deposit_all(&mut self, assets: I) + where + I: IntoIterator, + { + assets.into_iter().for_each(move |a| self.deposit(a)); + } + + /// Withdraws `asset` from the balance state returning `false` if it would overdraw the balance. + fn withdraw(&mut self, asset: Asset) -> bool; + + /// Withdraws every asset in `assets` from the balance state, returning `false` if it would + /// overdraw the balance. + #[inline] + fn withdraw_all(&mut self, assets: I) -> bool + where + I: IntoIterator, + { + for asset in AssetList::from_iter(assets) { + if !self.withdraw(asset) { + return false; + } + } + true + } + + /// Clears the entire balance state. + fn clear(&mut self); +} + +impl BalanceState for AssetList { + #[inline] + fn balance(&self, id: AssetId) -> AssetValue { + self.value(id) + } + + #[allow(clippy::only_used_in_recursion)] // NOTE: False-positive: rust-clippy/issues/8560 + #[inline] + fn deposit(&mut self, asset: Asset) { + self.deposit(asset); + } + + #[allow(clippy::only_used_in_recursion)] // NOTE: False-positive: rust-clippy/issues/8560 + #[inline] + fn withdraw(&mut self, asset: Asset) -> bool { + self.withdraw(asset) + } + + #[inline] + fn clear(&mut self) { + self.clear(); + } +} + +/// Performs a withdraw on `balance` returning `false` if it would overflow. +#[inline] +fn withdraw(balance: Option<&mut AssetValue>, withdraw: AssetValue) -> bool { + match balance { + Some(balance) => { + *balance = match balance.checked_sub(withdraw) { + Some(balance) => balance, + _ => return false, + }; + true + } + _ => false, + } +} + +/// Adds implementation of [`BalanceState`] for a map type with the given `$entry` type. +macro_rules! impl_balance_state_map_body { + ($entry:tt) => { + #[inline] + fn balance(&self, id: AssetId) -> AssetValue { + self.get(&id).copied().unwrap_or_default() + } + + #[inline] + fn deposit(&mut self, asset: Asset) { + if asset.is_zero() { + return; + } + match self.entry(asset.id) { + $entry::Vacant(entry) => { + entry.insert(asset.value); + } + $entry::Occupied(entry) => *entry.into_mut() += asset.value, + } + } + + #[inline] + fn withdraw(&mut self, asset: Asset) -> bool { + if !asset.is_zero() { + withdraw(self.get_mut(&asset.id), asset.value) + } else { + true + } + } + + #[inline] + fn clear(&mut self) { + self.clear(); + } + }; +} + +/// B-Tree Map [`BalanceState`] Implementation +pub type BTreeMapBalanceState = BTreeMap; + +impl BalanceState for BTreeMapBalanceState { + impl_balance_state_map_body! { BTreeMapEntry } +} + +/// Hash Map [`BalanceState`] Implementation +#[cfg(feature = "std")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] +pub type HashMapBalanceState = HashMap; + +#[cfg(feature = "std")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] +impl BalanceState for HashMapBalanceState +where + S: BuildHasher + Default, +{ + impl_balance_state_map_body! { HashMapEntry } +} + +/// Testing Framework +#[cfg(any(feature = "test", test))] +#[cfg_attr(doc_cfg, doc(cfg(feature = "test")))] +pub mod test { + use super::*; + use manta_crypto::rand::{CryptoRng, RngCore, Sample}; + + #[cfg(test)] + use manta_crypto::rand::OsRng; + + /// Asserts that a random deposit and withdraw is always valid. + #[inline] + pub fn assert_valid_withdraw(state: &mut S, rng: &mut R) + where + S: BalanceState, + R: CryptoRng + RngCore + ?Sized, + { + let asset = Asset::gen(rng); + let initial_balance = state.balance(asset.id); + state.deposit(asset); + assert_eq!( + initial_balance + asset.value, + state.balance(asset.id), + "Current balance and sum of initial balance and new deposit should have been equal." + ); + state.withdraw(asset); + assert_eq!( + initial_balance, + state.balance(asset.id), + "Initial and final balances should have been equal." + ); + } + + /// Tests valid withdrawals for an [`AssetList`] balance state. + #[test] + fn asset_list_valid_withdraw() { + assert_valid_withdraw(&mut AssetList::new(), &mut OsRng); + } + + /// Tests valid withdrawals for a [`BTreeMapBalanceState`] balance state. + #[test] + fn btree_map_valid_withdraw() { + assert_valid_withdraw(&mut BTreeMapBalanceState::new(), &mut OsRng); + } + + /// Tests valid withdrawals for a [`HashMapBalanceState`] balance state. + #[cfg(feature = "std")] + #[test] + fn hash_map_valid_withdraw() { + assert_valid_withdraw(&mut HashMapBalanceState::new(), &mut OsRng); + } +} diff --git a/manta-accounting/src/wallet/mod.rs b/manta-accounting/src/wallet/mod.rs index 123ef9698..665aa349a 100644 --- a/manta-accounting/src/wallet/mod.rs +++ b/manta-accounting/src/wallet/mod.rs @@ -29,12 +29,13 @@ //! [`Ledger`]: ledger::Connection use crate::{ - asset::{Asset, AssetId, AssetList, AssetMetadata, AssetValue}, + asset::{Asset, AssetId, AssetMetadata, AssetValue}, transfer::{ canonical::{Transaction, TransactionKind}, Configuration, ReceivingKey, }, wallet::{ + balance::{BTreeMapBalanceState, BalanceState}, ledger::{Checkpoint, PullResponse, PushResponse}, signer::{ ReceivingKeyRequest, SignError, SignRequest, SignResponse, SyncError, SyncRequest, @@ -42,22 +43,14 @@ use crate::{ }, }, }; -use alloc::{ - collections::btree_map::{BTreeMap, Entry as BTreeMapEntry}, - vec::Vec, -}; +use alloc::vec::Vec; use core::{fmt::Debug, hash::Hash, marker::PhantomData}; use manta_util::ops::ControlFlow; #[cfg(feature = "serde")] use manta_util::serde::{Deserialize, Serialize}; -#[cfg(feature = "std")] -use std::{ - collections::hash_map::{Entry as HashMapEntry, HashMap, RandomState}, - hash::BuildHasher, -}; - +pub mod balance; pub mod ledger; pub mod signer; @@ -65,147 +58,6 @@ pub mod signer; #[cfg_attr(doc_cfg, doc(cfg(feature = "test")))] pub mod test; -/// Balance State -pub trait BalanceState: Default { - /// Returns the current balance associated with this `id`. - fn balance(&self, id: AssetId) -> AssetValue; - - /// Returns true if `self` contains at least `asset.value` of the asset of kind `asset.id`. - #[inline] - fn contains(&self, asset: Asset) -> bool { - self.balance(asset.id) >= asset.value - } - - /// Deposits `asset` into the balance state, increasing the balance of the asset stored at - /// `asset.id` by an amount equal to `asset.value`. - fn deposit(&mut self, asset: Asset); - - /// Deposits every asset in `assets` into the balance state. - #[inline] - fn deposit_all(&mut self, assets: I) - where - I: IntoIterator, - { - assets.into_iter().for_each(move |a| self.deposit(a)); - } - - /// Withdraws `asset` from the balance state returning `false` if it would overdraw the balance. - fn withdraw(&mut self, asset: Asset) -> bool; - - /// Withdraws every asset in `assets` from the balance state, returning `false` if it would - /// overdraw the balance. - #[inline] - fn withdraw_all(&mut self, assets: I) -> bool - where - I: IntoIterator, - { - for asset in AssetList::from_iter(assets) { - if !self.withdraw(asset) { - return false; - } - } - true - } - - /// Clears the entire balance state. - fn clear(&mut self); -} - -impl BalanceState for AssetList { - #[inline] - fn balance(&self, id: AssetId) -> AssetValue { - self.value(id) - } - - #[inline] - fn deposit(&mut self, asset: Asset) { - self.deposit(asset) - } - - #[inline] - fn withdraw(&mut self, asset: Asset) -> bool { - self.withdraw(asset) - } - - #[inline] - fn clear(&mut self) { - self.clear(); - } -} - -/// Performs a withdraw on `balance` returning `false` if it would overflow. -#[inline] -fn withdraw(balance: Option<&mut AssetValue>, withdraw: AssetValue) -> bool { - match balance { - Some(balance) => { - *balance = match balance.checked_sub(withdraw) { - Some(balance) => balance, - _ => return false, - }; - true - } - _ => false, - } -} - -/// Adds implementation of [`BalanceState`] for a map type with the given `$entry` type. -macro_rules! impl_balance_state_map_body { - ($entry:tt) => { - #[inline] - fn balance(&self, id: AssetId) -> AssetValue { - self.get(&id).copied().unwrap_or_default() - } - - #[inline] - fn deposit(&mut self, asset: Asset) { - if asset.is_zero() { - return; - } - match self.entry(asset.id) { - $entry::Vacant(entry) => { - entry.insert(asset.value); - } - $entry::Occupied(entry) => *entry.into_mut() += asset.value, - } - } - - #[inline] - fn withdraw(&mut self, asset: Asset) -> bool { - if !asset.is_zero() { - withdraw(self.get_mut(&asset.id), asset.value) - } else { - true - } - } - - #[inline] - fn clear(&mut self) { - self.clear(); - } - }; -} - -/// B-Tree Map [`BalanceState`] Implementation -pub type BTreeMapBalanceState = BTreeMap; - -impl BalanceState for BTreeMapBalanceState { - impl_balance_state_map_body! { BTreeMapEntry } -} - -/// Hash Map [`BalanceState`] Implementation -#[cfg(feature = "std")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] -pub type HashMapBalanceState = HashMap; - -#[cfg(feature = "std")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] -impl BalanceState for HashMapBalanceState -where - S: BuildHasher + Default, -{ - impl_balance_state_map_body! { HashMapEntry } -} - /// Wallet pub struct Wallet, B = BTreeMapBalanceState> where