diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f3c769b3..2c89791ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: steps: - uses: actions/checkout@v3 - run: rustup update nightly && rustup default nightly - - run: RUSTDOCFLAGS="-D warnings --cfg doc_cfg" cargo doc --workspace --all-features --no-deps + - run: RUSTDOCFLAGS="-D warnings --cfg doc_cfg" cargo doc --workspace --all-features --no-deps --document-private-items bench: name: Bench (${{ matrix.os }}) strategy: diff --git a/.github/workflows/simulation.yml b/.github/workflows/simulation.yml new file mode 100644 index 000000000..895d00bf6 --- /dev/null +++ b/.github/workflows/simulation.yml @@ -0,0 +1,33 @@ +name: Simulation +on: + workflow_dispatch: +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + RUST_BACKTRACE: full +jobs: + simulation: + name: Simulation (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + agents: + - 1 + - 2 + - 3 + - 10 + - 100 + rounds: + - 10 + - 100 + - 1000 + - 10000 + os: + - ubuntu-latest + channel: + - nightly + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - run: rustup update ${{ matrix.channel }} --no-self-update && rustup default ${{ matrix.channel }} + - run: cargo run --package manta-pay --all-features --release --bin simulation ${{ matrix.agents }} ${{ matrix.rounds }} 10 100000 diff --git a/manta-accounting/Cargo.toml b/manta-accounting/Cargo.toml index 884e9ad58..0154034cc 100644 --- a/manta-accounting/Cargo.toml +++ b/manta-accounting/Cargo.toml @@ -47,8 +47,8 @@ std = ["manta-crypto/std", "manta-util/std"] test = [ "futures", "indexmap", + "manta-crypto/rand", "parking_lot", - "rand/alloc", "statrs" ] @@ -62,7 +62,6 @@ indexmap = { version = "1.8.0", optional = true, default-features = false } manta-crypto = { path = "../manta-crypto", default-features = false } manta-util = { path = "../manta-util", default-features = false, features = ["alloc"] } parking_lot = { version = "0.12.0", optional = true, default-features = false } -rand = { version = "0.8.4", optional = true, default-features = false } rand_chacha = { version = "0.3.1", optional = true, default-features = false } statrs = { version = "0.15.0", optional = true, default-features = false } diff --git a/manta-accounting/src/asset.rs b/manta-accounting/src/asset.rs index 8d22faeab..8f609905d 100644 --- a/manta-accounting/src/asset.rs +++ b/manta-accounting/src/asset.rs @@ -98,6 +98,17 @@ impl AssetId { pub const fn into_bytes(self) -> [u8; Self::SIZE] { self.0.to_le_bytes() } + + /// Samples an [`Asset`] by uniformly choosing between zero and `maximum` when selecting coins. + #[cfg(feature = "test")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "test")))] + #[inline] + pub fn sample_up_to(self, maximum: AssetValue, rng: &mut R) -> Asset + where + R: CryptoRng + RngCore + ?Sized, + { + self.value(rng.gen_range(0..maximum.0)) + } } impl From for [u8; AssetId::SIZE] { diff --git a/manta-accounting/src/transfer/canonical.rs b/manta-accounting/src/transfer/canonical.rs index 5f66a5b29..f897f8d18 100644 --- a/manta-accounting/src/transfer/canonical.rs +++ b/manta-accounting/src/transfer/canonical.rs @@ -346,6 +346,16 @@ where } } + /// Returns `true` if `self` is a [`Transaction`] which transfers zero value. + #[inline] + pub fn is_zero(&self) -> bool { + match self { + Self::Mint(asset) => asset.is_zero(), + Self::PrivateTransfer(asset, _) => asset.is_zero(), + Self::Reclaim(asset) => asset.is_zero(), + } + } + /// Returns a transaction summary given the asset `metadata`. #[inline] pub fn display(&self, metadata: &AssetMetadata, f: F) -> String diff --git a/manta-accounting/src/wallet/balance.rs b/manta-accounting/src/wallet/balance.rs index 8e7c13eda..62d30c88e 100644 --- a/manta-accounting/src/wallet/balance.rs +++ b/manta-accounting/src/wallet/balance.rs @@ -99,21 +99,6 @@ impl BalanceState for AssetList { } } -/// 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) => { @@ -138,7 +123,18 @@ macro_rules! impl_balance_state_map_body { #[inline] fn withdraw(&mut self, asset: Asset) -> bool { if !asset.is_zero() { - withdraw(self.get_mut(&asset.id), asset.value) + if let $entry::Occupied(mut entry) = self.entry(asset.id) { + let balance = entry.get_mut(); + if let Some(next_balance) = balance.checked_sub(asset.value) { + if next_balance == 0 { + entry.remove(); + } else { + *balance = next_balance; + } + return true; + } + } + false } else { true } @@ -205,22 +201,97 @@ pub mod test { ); } - /// Tests valid withdrawals for an [`AssetList`] balance state. - #[test] - fn asset_list_valid_withdraw() { - assert_valid_withdraw(&mut AssetList::new(), &mut OsRng); + /// Asserts that a maximal withdraw that leaves the state with no value should delete its memory + /// for this process. + #[inline] + pub fn assert_full_withdraw_should_remove_entry(rng: &mut R) + where + S: BalanceState, + for<'s> &'s S: IntoIterator, + for<'s> <&'s S as IntoIterator>::IntoIter: ExactSizeIterator, + R: CryptoRng + RngCore + ?Sized, + { + let mut state = S::default(); + let asset = Asset::gen(rng); + let initial_length = state.into_iter().len(); + state.deposit(asset); + assert_eq!( + initial_length + 1, + state.into_iter().len(), + "Length should have increased by one after depositing a new asset." + ); + let balance = state.balance(asset.id); + state.withdraw(asset.id.with(balance)); + assert_eq!( + state.balance(asset.id), + 0, + "Balance in the removed AssetId should be zero." + ); + assert_eq!( + initial_length, + state.into_iter().len(), + "Removed AssetId should remove its entry in the database." + ); } - /// Tests valid withdrawals for a [`BTreeMapBalanceState`] balance state. - #[test] - fn btree_map_valid_withdraw() { - assert_valid_withdraw(&mut BTreeMapBalanceState::new(), &mut OsRng); + /// Defines the tests across multiple different [`BalanceState`] types. + macro_rules! define_tests { + ($(( + $type:ty, + $doc:expr, + $valid_withdraw:ident, + $full_withdraw:ident + $(,)?)),*$(,)?) => { + $( + #[doc = "Tests valid withdrawals for an"] + #[doc = $doc] + #[doc = "balance state."] + #[test] + fn $valid_withdraw() { + let mut state = <$type>::default(); + let mut rng = OsRng; + for _ in 0..0xFFFF { + assert_valid_withdraw(&mut state, &mut rng); + } + } + + #[doc = "Tests that there are no empty entries in"] + #[doc = $doc] + #[doc = "with no value stored in them."] + #[test] + fn $full_withdraw() { + assert_full_withdraw_should_remove_entry::<$type, _>(&mut OsRng); + } + )* + } } + define_tests!( + ( + AssetList, + "[`AssetList`]", + asset_list_valid_withdraw, + asset_list_full_withdraw, + ), + ( + BTreeMapBalanceState, + "[`BTreeMapBalanceState`]", + btree_map_valid_withdraw, + btree_map_full_withdraw, + ), + ); + /// 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); } + + /// + #[cfg(feature = "std")] + #[test] + fn hash_map_full_withdraw() { + assert_full_withdraw_should_remove_entry::(&mut OsRng); + } } diff --git a/manta-accounting/src/wallet/test/mod.rs b/manta-accounting/src/wallet/test/mod.rs index d84291530..808671060 100644 --- a/manta-accounting/src/wallet/test/mod.rs +++ b/manta-accounting/src/wallet/test/mod.rs @@ -32,11 +32,16 @@ use alloc::{boxed::Box, sync::Arc, vec::Vec}; use core::{fmt::Debug, future::Future, hash::Hash, marker::PhantomData}; use futures::StreamExt; use indexmap::IndexSet; -use manta_crypto::rand::{CryptoRng, RngCore, Sample}; -use manta_util::future::LocalBoxFuture; +use manta_crypto::rand::{CryptoRng, Distribution, Rand, RngCore, Sample}; +use manta_util::{future::LocalBoxFuture, vec::VecExt}; use parking_lot::Mutex; -use rand::{distributions::Distribution, Rng}; -use statrs::{distribution::Categorical, StatsError}; +use statrs::{ + distribution::{Categorical, Poisson}, + StatsError, +}; + +#[cfg(feature = "serde")] +use manta_util::serde::{Deserialize, Serialize}; pub mod sim; @@ -48,16 +53,125 @@ where /// No Action Skip, - /// Post Transaction - Post(Transaction), + /// Post Transaction Data + Post { + /// Flag set to `true` whenever the `transaction` only rebalanaces internal assets without + /// sending them out to another agent. If this state is unknown, `false` should be chosen. + is_self: bool, + + /// Flag set to `true` whenever the `transaction` moves all assets in or out of the private + /// balance entirely. If this state is unknown, `false` should be chosen. + is_maximal: bool, - /// Generate Public Key - GeneratePublicKey, + /// Transaction Data + transaction: Transaction, + }, + + /// Generate Receiving Keys + GenerateReceivingKeys { + /// Number of Keys to Generate + count: usize, + }, /// Recover Wallet Recover, } +impl Action +where + C: transfer::Configuration, +{ + /// Generates a [`Post`](Self::Post) on `transaction` self-pointed if `is_self` is `true` and + /// maximal if `is_maximal` is `true`. + #[inline] + pub fn post(is_self: bool, is_maximal: bool, transaction: Transaction) -> Self { + Self::Post { + is_self, + is_maximal, + transaction, + } + } + + /// Generates a [`Post`](Self::Post) on `transaction` self-pointed which is maximal if + /// `is_maximal` is `true`. + #[inline] + pub fn self_post(is_maximal: bool, transaction: Transaction) -> Self { + Self::post(true, is_maximal, transaction) + } + + /// Generates a [`Transaction::Mint`] for `asset`. + #[inline] + pub fn mint(asset: Asset) -> Self { + Self::self_post(false, Transaction::Mint(asset)) + } + + /// Generates a [`Transaction::PrivateTransfer`] for `asset` to `key` self-pointed if `is_self` + /// is `true`. + #[inline] + pub fn private_transfer(is_self: bool, asset: Asset, key: ReceivingKey) -> Self { + Self::post(is_self, false, Transaction::PrivateTransfer(asset, key)) + } + + /// Generates a [`Transaction::Reclaim`] for `asset` which is maximal if `is_maximal` is `true`. + #[inline] + pub fn reclaim(is_maximal: bool, asset: Asset) -> Self { + Self::self_post(is_maximal, Transaction::Reclaim(asset)) + } + + /// Computes the [`ActionType`] for a [`Post`](Self::Post) type with the `is_self`, + /// `is_maximal`, and `transaction` parameters. + #[inline] + pub fn as_post_type( + is_self: bool, + is_maximal: bool, + transaction: &Transaction, + ) -> ActionType { + use Transaction::*; + match (is_self, is_maximal, transaction.is_zero(), transaction) { + (_, _, true, Mint { .. }) => ActionType::MintZero, + (_, _, false, Mint { .. }) => ActionType::Mint, + (true, _, true, PrivateTransfer { .. }) => ActionType::SelfTransferZero, + (true, _, false, PrivateTransfer { .. }) => ActionType::SelfTransfer, + (false, _, true, PrivateTransfer { .. }) => ActionType::PrivateTransferZero, + (false, _, false, PrivateTransfer { .. }) => ActionType::PrivateTransfer, + (_, true, _, Reclaim { .. }) => ActionType::FlushToPublic, + (_, false, true, Reclaim { .. }) => ActionType::ReclaimZero, + (_, false, false, Reclaim { .. }) => ActionType::Reclaim, + } + } + + /// Converts `self` into its corresponding [`ActionType`]. + #[inline] + pub fn as_type(&self) -> ActionType { + match self { + Self::Skip => ActionType::Skip, + Self::Post { + is_self, + is_maximal, + transaction, + } => Self::as_post_type(*is_self, *is_maximal, transaction), + Self::GenerateReceivingKeys { .. } => ActionType::GenerateReceivingKeys, + Self::Recover => ActionType::Recover, + } + } +} + +/// Action Labelled Data +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct ActionLabelled { + /// Action Type + pub action: ActionType, + + /// Data Value + pub value: T, +} + +/// [ActionLabelled`] Error Type +pub type ActionLabelledError = ActionLabelled>; + +/// Possible [`Action`] or an [`ActionLabelledError`] Variant +pub type MaybeAction = Result, ActionLabelledError>; + /// Action Types #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum ActionType { @@ -67,20 +181,54 @@ pub enum ActionType { /// Mint Action Mint, + /// Mint Zero Action + MintZero, + /// Private Transfer Action PrivateTransfer, + /// Private Transfer Zero Action + PrivateTransferZero, + /// Reclaim Action Reclaim, - /// Generate Public Key Action - GeneratePublicKey, + /// Reclaim Zero Action + ReclaimZero, - /// Recover Wallet + /// Self Private Transfer Action + SelfTransfer, + + /// Self Private Transfer Zero Action + SelfTransferZero, + + /// Flush-to-Public Transfer Action + FlushToPublic, + + /// Generate Receiving Keys Action + GenerateReceivingKeys, + + /// Recover Wallet Action Recover, } +impl ActionType { + /// Generates an [`ActionLabelled`] type over `value` with `self` as the [`ActionType`]. + #[inline] + pub fn label(self, value: T) -> ActionLabelled { + ActionLabelled { + action: self, + value, + } + } +} + /// Action Distribution Probability Mass Function +#[cfg_attr( + feature = "serde", + derive(Deserialize, Serialize), + serde(crate = "manta_util::serde", deny_unknown_fields) +)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct ActionDistributionPMF { /// No Action Weight @@ -89,16 +237,34 @@ pub struct ActionDistributionPMF { /// Mint Action Weight pub mint: T, + /// Mint Zero Action Weight + pub mint_zero: T, + /// Private Transfer Action Weight pub private_transfer: T, + /// Private Transfer Zero Action Weight + pub private_transfer_zero: T, + /// Reclaim Action Weight pub reclaim: T, - /// Generate Public Key Weight - pub generate_public_key: T, + /// Reclaim Action Zero Weight + pub reclaim_zero: T, + + /// Self Private Transfer Action Weight + pub self_transfer: T, - /// Recover Wallet Weight + /// Self Private Transfer Zero Action Weight + pub self_transfer_zero: T, + + /// Flush-to-Public Transfer Action Weight + pub flush_to_public: T, + + /// Generate Receiving Keys Action Weight + pub generate_receiving_keys: T, + + /// Recover Wallet Action Weight pub recover: T, } @@ -108,9 +274,15 @@ impl Default for ActionDistributionPMF { Self { skip: 2, mint: 5, + mint_zero: 1, private_transfer: 9, + private_transfer_zero: 1, reclaim: 3, - generate_public_key: 3, + reclaim_zero: 1, + self_transfer: 2, + self_transfer_zero: 1, + flush_to_public: 1, + generate_receiving_keys: 3, recover: 4, } } @@ -140,9 +312,15 @@ impl TryFrom for ActionDistribution { distribution: Categorical::new(&[ pmf.skip as f64, pmf.mint as f64, + pmf.mint_zero as f64, pmf.private_transfer as f64, + pmf.private_transfer_zero as f64, pmf.reclaim as f64, - pmf.generate_public_key as f64, + pmf.reclaim_zero as f64, + pmf.self_transfer as f64, + pmf.self_transfer_zero as f64, + pmf.flush_to_public as f64, + pmf.generate_receiving_keys as f64, pmf.recover as f64, ])?, }) @@ -158,10 +336,16 @@ impl Distribution for ActionDistribution { match self.distribution.sample(rng) as usize { 0 => ActionType::Skip, 1 => ActionType::Mint, - 2 => ActionType::PrivateTransfer, - 3 => ActionType::Reclaim, - 4 => ActionType::GeneratePublicKey, - 5 => ActionType::Recover, + 2 => ActionType::MintZero, + 3 => ActionType::PrivateTransfer, + 4 => ActionType::PrivateTransferZero, + 5 => ActionType::Reclaim, + 6 => ActionType::ReclaimZero, + 7 => ActionType::SelfTransfer, + 8 => ActionType::SelfTransferZero, + 9 => ActionType::FlushToPublic, + 10 => ActionType::GenerateReceivingKeys, + 11 => ActionType::Recover, _ => unreachable!(), } } @@ -241,21 +425,49 @@ where Some(()) } + /// Returns the default receiving key for `self`. + #[inline] + async fn default_receiving_key(&mut self) -> Result, Error> { + self.wallet + .receiving_keys(ReceivingKeyRequest::Get { + index: Default::default(), + }) + .await + .map_err(Error::SignerConnectionError) + .map(Vec::take_first) + } + + /// Returns the latest public balances from the ledger. + #[inline] + async fn public_balances(&mut self) -> Result, Error> + where + L: PublicBalanceOracle, + { + self.wallet.sync().await?; + Ok(self.wallet.ledger().public_balances().await) + } + + /// Synchronizes with the ledger, attaching the `action` marker for the possible error branch. + #[inline] + async fn sync_with(&mut self, action: ActionType) -> Result<(), ActionLabelledError> { + self.wallet.sync().await.map_err(|err| action.label(err)) + } + /// Samples a deposit from `self` using `rng` returning `None` if no deposit is possible. #[inline] - async fn sample_deposit(&mut self, rng: &mut R) -> Option + async fn sample_deposit(&mut self, rng: &mut R) -> Result, Error> where L: PublicBalanceOracle, R: CryptoRng + RngCore + ?Sized, { - let _ = self.wallet.sync().await; - let assets = self.wallet.ledger().public_balances().await?; - let len = assets.len(); - if len == 0 { - return None; + let assets = match self.public_balances().await? { + Some(assets) => assets, + _ => return Ok(None), + }; + match rng.select_item(assets) { + Some(asset) => Ok(Some(asset.id.sample_up_to(asset.value, rng))), + _ => Ok(None), } - let asset = assets.iter().nth(rng.gen_range(0..len)).unwrap(); - Some(asset.id.value(rng.gen_range(0..asset.value.0))) } /// Samples a withdraw from `self` using `rng` returning `None` if no withdrawal is possible. @@ -265,18 +477,193 @@ where /// This method samples from a uniform distribution over the asset IDs and asset values present /// in the balance state of `self`. #[inline] - async fn sample_withdraw(&mut self, rng: &mut R) -> Option + async fn sample_withdraw(&mut self, rng: &mut R) -> Result, Error> where R: CryptoRng + RngCore + ?Sized, { - let _ = self.wallet.sync().await; - let assets = self.wallet.assets(); - let len = assets.len(); - if len == 0 { - return None; + self.wallet.sync().await?; + match rng.select_item(self.wallet.assets()) { + Some((id, value)) => Ok(Some(id.sample_up_to(*value, rng))), + _ => Ok(None), } - let (asset_id, asset_value) = assets.iter().nth(rng.gen_range(0..len)).unwrap(); - Some(asset_id.value(rng.gen_range(0..asset_value.0))) + } + + /// Samples an asset balance from the wallet of `self`, labelling the possible error with + /// `action` if it occurs during synchronization. + #[inline] + async fn sample_asset( + &mut self, + action: ActionType, + rng: &mut R, + ) -> Result, ActionLabelledError> + where + R: CryptoRng + RngCore + ?Sized, + { + self.sync_with(action).await?; + Ok(rng + .select_item(self.wallet.assets()) + .map(|(id, value)| Asset::new(*id, *value))) + } + + /// Samples a [`Mint`] against `self` using `rng`, returning a [`Skip`] if [`Mint`] is + /// impossible. + /// + /// [`Mint`]: ActionType::Mint + /// [`Skip`]: ActionType::Skip + #[inline] + async fn sample_mint(&mut self, rng: &mut R) -> MaybeAction + where + L: PublicBalanceOracle, + R: CryptoRng + RngCore + ?Sized, + { + match self.sample_deposit(rng).await { + Ok(Some(asset)) => Ok(Action::mint(asset)), + Ok(_) => Ok(Action::Skip), + Err(err) => Err(ActionType::Mint.label(err)), + } + } + + /// Samples a [`MintZero`] against `self` using `rng` to select the [`AssetId`], returning + /// a [`Skip`] if [`MintZero`] is impossible. + /// + /// [`MintZero`]: ActionType::MintZero + /// [`AssetId`]: crate::asset::AssetId + /// [`Skip`]: ActionType::Skip + #[inline] + async fn sample_zero_mint(&mut self, rng: &mut R) -> MaybeAction + where + L: PublicBalanceOracle, + R: CryptoRng + RngCore + ?Sized, + { + match self.public_balances().await { + Ok(Some(assets)) => match rng.select_item(assets) { + Some(asset) => Ok(Action::mint(asset.id.value(0))), + _ => Ok(Action::Skip), + }, + Ok(_) => Ok(Action::Skip), + Err(err) => Err(ActionType::MintZero.label(err)), + } + } + + /// Samples a [`PrivateTransfer`] against `self` using `rng`, returning a [`Mint`] if + /// [`PrivateTransfer`] is impossible and then a [`Skip`] if the [`Mint`] is impossible. + /// + /// [`PrivateTransfer`]: ActionType::PrivateTransfer + /// [`Mint`]: ActionType::Mint + /// [`Skip`]: ActionType::Skip + #[inline] + async fn sample_private_transfer( + &mut self, + is_self: bool, + rng: &mut R, + key: K, + ) -> MaybeAction + where + L: PublicBalanceOracle, + R: CryptoRng + RngCore + ?Sized, + K: FnOnce(&mut R) -> Result>, Error>, + { + let action = if is_self { + ActionType::SelfTransfer + } else { + ActionType::PrivateTransfer + }; + match self.sample_withdraw(rng).await { + Ok(Some(asset)) => match key(rng) { + Ok(Some(key)) => Ok(Action::private_transfer(is_self, asset, key)), + Ok(_) => Ok(Action::GenerateReceivingKeys { count: 1 }), + Err(err) => Err(action.label(err)), + }, + Ok(_) => self.sample_mint(rng).await, + Err(err) => Err(action.label(err)), + } + } + + /// Samples a [`PrivateTransferZero`] against `self` using an `rng`, returning a [`Mint`] if + /// [`PrivateTransfer`] is impossible and then a [`Skip`] if the [`Mint`] is impossible. + /// + /// [`PrivateTransferZero`]: ActionType::PrivateTransferZero + /// [`PrivateTransfer`]: ActionType::PrivateTransfer + /// [`Mint`]: ActionType::Mint + /// [`Skip`]: ActionType::Skip + #[inline] + async fn sample_zero_private_transfer( + &mut self, + is_self: bool, + rng: &mut R, + key: K, + ) -> MaybeAction + where + L: PublicBalanceOracle, + R: CryptoRng + RngCore + ?Sized, + K: FnOnce(&mut R) -> Result>, Error>, + { + let action = if is_self { + ActionType::SelfTransfer + } else { + ActionType::PrivateTransfer + }; + match self.sample_asset(action, rng).await { + Ok(Some(asset)) => match key(rng) { + Ok(Some(key)) => Ok(Action::private_transfer(is_self, asset.id.value(0), key)), + Ok(_) => Ok(Action::GenerateReceivingKeys { count: 1 }), + Err(err) => Err(action.label(err)), + }, + Ok(_) => Ok(self.sample_zero_mint(rng).await?), + Err(err) => Err(err), + } + } + + /// Samples a [`Reclaim`] against `self` using `rng`, returning a [`Skip`] if [`Reclaim`] is + /// impossible. + /// + /// [`Reclaim`]: ActionType::Reclaim + /// [`Skip`]: ActionType::Skip + #[inline] + async fn sample_reclaim(&mut self, rng: &mut R) -> MaybeAction + where + L: PublicBalanceOracle, + R: CryptoRng + RngCore + ?Sized, + { + match self.sample_withdraw(rng).await { + Ok(Some(asset)) => Ok(Action::reclaim(false, asset)), + Ok(_) => self.sample_mint(rng).await, + Err(err) => Err(ActionType::Reclaim.label(err)), + } + } + + /// Samples a [`ReclaimZero`] against `self` using `rng`, returning a [`Skip`] if + /// [`ReclaimZero`] is impossible. + /// + /// [`ReclaimZero`]: ActionType::ReclaimZero + /// [`Skip`]: ActionType::Skip + #[inline] + async fn sample_zero_reclaim(&mut self, rng: &mut R) -> MaybeAction + where + R: CryptoRng + RngCore + ?Sized, + { + Ok(self + .sample_asset(ActionType::ReclaimZero, rng) + .await? + .map(|asset| Action::reclaim(false, asset.id.value(0))) + .unwrap_or(Action::Skip)) + } + + /// Reclaims all of the private balance of a random [`AssetId`] to public balance or [`Skip`] if + /// the private balance is empty. + /// + /// [`AssetId`]: crate::asset::AssetId + /// [`Skip`]: ActionType::Skip + #[inline] + async fn flush_to_public(&mut self, rng: &mut R) -> MaybeAction + where + R: CryptoRng + RngCore + ?Sized, + { + Ok(self + .sample_asset(ActionType::FlushToPublic, rng) + .await? + .map(|asset| Action::reclaim(true, asset)) + .unwrap_or(Action::Skip)) } /// Computes the current balance state of the wallet, performs a full recovery, and then @@ -293,26 +680,14 @@ where } /// Simulation Event -#[derive(derivative::Derivative)] -#[derivative(Debug(bound = "L::Response: Debug, Error: Debug"))] -pub struct Event -where - C: transfer::Configuration, - L: Ledger, - S: signer::Connection, -{ - /// Action Type - pub action: ActionType, - - /// Action Result - pub result: Result>, -} +pub type Event = + ActionLabelled>>>::Response, Error>>; -/// Public Key Database -pub type PublicKeyDatabase = IndexSet>; +/// Receiving Key Database +pub type ReceivingKeyDatabase = IndexSet>; -/// Shared Public Key Database -pub type SharedPublicKeyDatabase = Arc>>; +/// Shared Receiving Key Database +pub type SharedReceivingKeyDatabase = Arc>>; /// Simulation #[derive(derivative::Derivative)] @@ -324,8 +699,8 @@ where S: signer::Connection, PublicKey: Eq + Hash, { - /// Public Key Database - public_keys: SharedPublicKeyDatabase, + /// Receiving Key Database + receiving_keys: SharedReceivingKeyDatabase, /// Type Parameter Marker __: PhantomData<(L, S)>, @@ -342,10 +717,20 @@ where #[inline] pub fn new(keys: [ReceivingKey; N]) -> Self { Self { - public_keys: Arc::new(Mutex::new(keys.into_iter().collect())), + receiving_keys: Arc::new(Mutex::new(keys.into_iter().collect())), __: PhantomData, } } + + /// Samples a random receiving key from + #[inline] + pub fn sample_receiving_key(&self, rng: &mut R) -> Option> + where + R: CryptoRng + RngCore + ?Sized, + { + rng.select_item(self.receiving_keys.lock().iter()) + .map(Clone::clone) + } } impl sim::ActionSimulation for Simulation @@ -356,7 +741,7 @@ where PublicKey: Eq + Hash, { type Actor = Actor; - type Action = Action; + type Action = MaybeAction; type Event = Event; #[inline] @@ -372,38 +757,45 @@ where actor.reduce_lifetime()?; let action = actor.distribution.sample(rng); Some(match action { - ActionType::Skip => Action::Skip, - ActionType::Mint => match actor.sample_deposit(rng).await { - Some(asset) => Action::Post(Transaction::Mint(asset)), - _ => Action::Skip, - }, - ActionType::PrivateTransfer => match actor.sample_withdraw(rng).await { - Some(asset) => { - let public_keys = self.public_keys.lock(); - let len = public_keys.len(); - if len == 0 { - Action::GeneratePublicKey - } else { - Action::Post(Transaction::PrivateTransfer( - asset, - public_keys[rng.gen_range(0..len)].clone(), - )) - } - } - _ => match actor.sample_deposit(rng).await { - Some(asset) => Action::Post(Transaction::Mint(asset)), - _ => Action::Skip, - }, - }, - ActionType::Reclaim => match actor.sample_withdraw(rng).await { - Some(asset) => Action::Post(Transaction::Reclaim(asset)), - _ => match actor.sample_deposit(rng).await { - Some(asset) => Action::Post(Transaction::Mint(asset)), - _ => Action::Skip, - }, - }, - ActionType::GeneratePublicKey => Action::GeneratePublicKey, - ActionType::Recover => Action::Recover, + ActionType::Skip => Ok(Action::Skip), + ActionType::Mint => actor.sample_mint(rng).await, + ActionType::MintZero => actor.sample_zero_mint(rng).await, + ActionType::PrivateTransfer => { + actor + .sample_private_transfer(false, rng, |rng| { + Ok(self.sample_receiving_key(rng)) + }) + .await + } + ActionType::PrivateTransferZero => { + actor + .sample_zero_private_transfer(false, rng, |rng| { + Ok(self.sample_receiving_key(rng)) + }) + .await + } + ActionType::Reclaim => actor.sample_reclaim(rng).await, + ActionType::ReclaimZero => actor.sample_zero_reclaim(rng).await, + ActionType::SelfTransfer => { + let key = actor.default_receiving_key().await; + actor + .sample_private_transfer(true, rng, |_| key.map(Some)) + .await + } + ActionType::SelfTransferZero => { + let key = actor.default_receiving_key().await; + actor + .sample_zero_private_transfer(true, rng, |_| key.map(Some)) + .await + } + ActionType::FlushToPublic => actor.flush_to_public(rng).await, + ActionType::GenerateReceivingKeys => Ok(Action::GenerateReceivingKeys { + count: Poisson::new(1.0) + .expect("The Poisson parameter is greater than zero.") + .sample(rng) + .ceil() as usize, + }), + ActionType::Recover => Ok(Action::Recover), }) }) } @@ -416,50 +808,59 @@ where ) -> LocalBoxFuture<'s, Self::Event> { Box::pin(async move { match action { - Action::Skip => Event { - action: ActionType::Skip, - result: Ok(true), - }, - Action::Post(transaction) => { - let action = match &transaction { - Transaction::Mint(_) => ActionType::Mint, - Transaction::PrivateTransfer(_, _) => ActionType::PrivateTransfer, - Transaction::Reclaim(_) => ActionType::Reclaim, - }; - let mut retries = 5; // TODO: Make this parameter tunable based on concurrency. - loop { - let result = actor.wallet.post(transaction.clone(), None).await; - if let Ok(false) = result { - if retries == 0 { - break Event { action, result }; + Ok(action) => match action { + Action::Skip => Event { + action: ActionType::Skip, + value: Ok(true), + }, + Action::Post { + is_self, + is_maximal, + transaction, + } => { + let action = Action::as_post_type(is_self, is_maximal, &transaction); + let mut retries = 5; // TODO: Make this parameter tunable based on concurrency. + loop { + let event = Event { + action, + value: actor.wallet.post(transaction.clone(), None).await, + }; + if let Ok(false) = event.value { + if retries == 0 { + break event; + } else { + retries -= 1; + continue; + } } else { - retries -= 1; - continue; + break event; } - } else { - break Event { action, result }; } } - } - Action::GeneratePublicKey => Event { - action: ActionType::GeneratePublicKey, - result: match actor - .wallet - .receiving_keys(ReceivingKeyRequest::New { count: 1 }) - .await - { - Ok(keys) => { - for key in keys { - self.public_keys.lock().insert(key); + Action::GenerateReceivingKeys { count } => Event { + action: ActionType::GenerateReceivingKeys, + value: match actor + .wallet + .receiving_keys(ReceivingKeyRequest::New { count }) + .await + { + Ok(keys) => { + for key in keys { + self.receiving_keys.lock().insert(key); + } + Ok(true) } - Ok(true) - } - Err(err) => Err(Error::SignerConnectionError(err)), + Err(err) => Err(Error::SignerConnectionError(err)), + }, + }, + Action::Recover => Event { + action: ActionType::Recover, + value: actor.recover().await, }, }, - Action::Recover => Event { - action: ActionType::Recover, - result: actor.recover().await, + Err(err) => Event { + action: err.action, + value: Err(err.value), }, } }) diff --git a/manta-accounting/src/wallet/test/sim.rs b/manta-accounting/src/wallet/test/sim.rs index 4b70329b2..c068092f5 100644 --- a/manta-accounting/src/wallet/test/sim.rs +++ b/manta-accounting/src/wallet/test/sim.rs @@ -187,7 +187,7 @@ where S: Simulation, R: 's + CryptoRng + RngCore, { - /// Builds a new [`ActorIter`] from `simulation`, `actor_index, `actor`, and `rng`. + /// Builds a new [`ActorStream`] from `simulation`, `actor_index, `actor`, and `rng`. #[inline] fn new(simulation: &'s S, actor_index: usize, actor: &'s mut S::Actor, rng: R) -> Self { Self { diff --git a/manta-crypto/Cargo.toml b/manta-crypto/Cargo.toml index bf0719476..a91780a12 100644 --- a/manta-crypto/Cargo.toml +++ b/manta-crypto/Cargo.toml @@ -40,6 +40,7 @@ test = [] [dependencies] derivative = { version = "2.2.0", default-features = false, features = ["use_core"] } manta-util = { path = "../manta-util", default-features = false, features = ["alloc"] } +rand = { version = "0.8.4", optional = true, default-features = false, features = ["alloc"] } rand_core = { version = "0.6.3", default-features = false } [dev-dependencies] diff --git a/manta-crypto/src/rand.rs b/manta-crypto/src/rand.rs index 9fa6d0017..a325f0f68 100644 --- a/manta-crypto/src/rand.rs +++ b/manta-crypto/src/rand.rs @@ -22,6 +22,9 @@ use alloc::vec::Vec; use core::{fmt::Debug, hash::Hash, iter::repeat, marker::PhantomData}; use manta_util::into_array_unchecked; +#[cfg(feature = "serde")] +use manta_util::serde::{Deserialize, Serialize}; + pub use rand_core::{block, CryptoRng, Error, RngCore, SeedableRng}; #[cfg(feature = "getrandom")] @@ -29,6 +32,14 @@ pub use rand_core::{block, CryptoRng, Error, RngCore, SeedableRng}; #[doc(inline)] pub use rand_core::OsRng; +#[cfg(feature = "rand")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "rand")))] +#[doc(inline)] +pub use rand::distributions::{ + uniform::{SampleRange, SampleUniform}, + Distribution, +}; + /// Random Number Generator Sized Wrapper #[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct SizedRng<'r, R>( @@ -268,6 +279,36 @@ where } } +/// Distribution Sampled Value +/// +/// This wrapper type automatically implements [`Sample`] whenever the `rand` crate is in scope by +/// sampling from a `rand::Distribution`. +#[cfg_attr( + feature = "serde", + derive(Deserialize, Serialize), + serde(crate = "manta_util::serde", deny_unknown_fields) +)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Sampled( + /// Sampled Value + pub T, +); + +#[cfg(feature = "rand")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "rand")))] +impl Sample for Sampled +where + D: Distribution, +{ + #[inline] + fn sample(distribution: D, rng: &mut R) -> Self + where + R: CryptoRng + RngCore + ?Sized, + { + Self(distribution.sample(rng)) + } +} + /// Distribution Iterator pub struct DistIter<'r, D, T, R> where @@ -406,6 +447,35 @@ pub trait Rand: CryptoRng + RngCore { bytes } + /// Generates a random value in the given `range`. + #[cfg(feature = "rand")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "rand")))] + #[inline] + fn gen_range(&mut self, range: R) -> T + where + T: SampleUniform, + R: SampleRange, + { + rand::Rng::gen_range(self, range) + } + + /// Selects a random item from `iter` by sampling an index less than or equal to the length of + /// `iter` and then traversing to that element, returning it if it exists. + #[cfg(feature = "rand")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "rand")))] + #[inline] + fn select_item(&mut self, iter: I) -> Option + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + let mut iter = iter.into_iter(); + match iter.len() { + 0 => None, + n => iter.nth(self.gen_range(0..n)), + } + } + /// Seeds another random number generator `R` using entropy from `self`. #[inline] fn seed_rng(&mut self) -> Result diff --git a/manta-util/Cargo.toml b/manta-util/Cargo.toml index 221d87232..23e5eac58 100644 --- a/manta-util/Cargo.toml +++ b/manta-util/Cargo.toml @@ -39,7 +39,7 @@ std = ["alloc"] [dependencies] crossbeam-channel = { version = "0.5.4", optional = true, default-features = false } -rayon = { version = "1.5.1", optional = true, default-features = false } +rayon = { version = "1.5.3", optional = true, default-features = false } serde = { version = "1.0.137", optional = true, default-features = false, features = ["derive"] } serde_with = { version = "1.13.0", optional = true, default-features = false, features = ["macros"] } diff --git a/manta-util/src/lib.rs b/manta-util/src/lib.rs index 4a10604db..d932eb31e 100644 --- a/manta-util/src/lib.rs +++ b/manta-util/src/lib.rs @@ -37,6 +37,10 @@ pub mod ops; pub mod persistence; pub mod pointer; +#[cfg(feature = "alloc")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] +pub mod vec; + pub use array::*; pub use bytes::*; pub use sealed::*; diff --git a/manta-util/src/vec.rs b/manta-util/src/vec.rs new file mode 100644 index 000000000..d5d0c8f9c --- /dev/null +++ b/manta-util/src/vec.rs @@ -0,0 +1,43 @@ +// 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 . + +//! Vector Utilities + +use crate::create_seal; +use alloc::vec::Vec; + +create_seal! {} + +impl sealed::Sealed for Vec {} + +/// Vector Extension Trait +pub trait VecExt: Into> + sealed::Sealed + Sized { + /// Returns the `n`th element of `self`, dropping the rest of the vector. + #[inline] + fn take(self, n: usize) -> T { + let mut vec = self.into(); + vec.truncate(n + 1); + vec.remove(n) + } + + /// Returns the first element of `self`, dropping the rest of the vector. + #[inline] + fn take_first(self) -> T { + self.take(0) + } +} + +impl VecExt for Vec {}