diff --git a/CHANGELOG.md b/CHANGELOG.md index 98659601e..c9fc6f64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [\#353](https://github.com/Manta-Network/manta-rs/pull/353) Restore Merkle tree pruning for the wallet. ### Changed +- [\#356](https://github.com/Manta-Network/manta-rs/pull/356) Signer ToPublic optimization. ### Deprecated diff --git a/manta-accounting/src/wallet/signer/functions.rs b/manta-accounting/src/wallet/signer/functions.rs index 695ab6159..25c341056 100644 --- a/manta-accounting/src/wallet/signer/functions.rs +++ b/manta-accounting/src/wallet/signer/functions.rs @@ -24,7 +24,7 @@ use crate::{ batch::Join, canonical::{ MultiProvingContext, PrivateTransfer, PrivateTransferShape, Selection, ToPrivate, - ToPublic, Transaction, TransactionData, TransferShape, + ToPublic, ToPublicShape, Transaction, TransactionData, TransferShape, }, receiver::ReceiverPost, requires_authorization, @@ -45,6 +45,7 @@ use crate::{ }, }; use alloc::{vec, vec::Vec}; +use core::ops::SubAssign; use manta_crypto::{ accumulator::{ Accumulator, BatchInsertion, FromItemsAndWitnesses, ItemHashFunction, OptimizedAccumulator, @@ -807,6 +808,137 @@ where Ok(into_array_unchecked(final_presenders)) } +/// Performs a ToPublic transaction spending the assets in `selection`, +/// returning [`TransferPost`]s. +#[allow(clippy::too_many_arguments)] +#[inline] +fn compute_to_public_transaction( + accounts: &AccountTable, + assets: &C::AssetMap, + parameters: &Parameters, + proving_context: &MultiProvingContext, + asset_id: &C::AssetId, + sink_accounts: Vec, + selection: Selection, + utxo_accumulator: &mut C::UtxoAccumulator, + rng: &mut C::Rng, +) -> Result, SignError> +where + C: Configuration, + C::AssetValue: SubAssign, +{ + let Selection { + mut change, + mut pre_senders, + } = selection; + let mut posts = Vec::new(); + let mut iter = pre_senders + .into_iter() + .chunk_by::<{ ToPublicShape::SENDERS }>(); + for chunk in &mut iter { + let senders = array_map(chunk, |s| { + s.try_upgrade(parameters, utxo_accumulator) + .expect("Unable to upgrade expected UTXO.") + }); + process_to_public_senders( + accounts, + parameters, + proving_context, + asset_id, + senders, + sink_accounts.clone(), + utxo_accumulator, + &mut change, + &mut posts, + rng, + )?; + } + pre_senders = iter.remainder(); + if !pre_senders.is_empty() { + let final_senders = into_array_unchecked(prepare_final_pre_senders( + accounts, + assets, + utxo_accumulator, + parameters, + asset_id, + Default::default(), + pre_senders, + rng, + )?); + process_to_public_senders( + accounts, + parameters, + proving_context, + asset_id, + final_senders, + sink_accounts, + utxo_accumulator, + &mut change, + &mut posts, + rng, + )?; + } + Ok(SignResponse::new(posts)) +} + +/// Creates a to public [`TransferPost`] spending the assets held by `senders` and +/// attaches it to `post`. +#[allow(clippy::too_many_arguments)] +#[inline] +fn process_to_public_senders( + accounts: &AccountTable, + parameters: &Parameters, + proving_context: &MultiProvingContext, + asset_id: &C::AssetId, + senders: [Sender; ToPublicShape::SENDERS], + sink_accounts: Vec, + utxo_accumulator: &mut C::UtxoAccumulator, + change: &mut C::AssetValue, + posts: &mut Vec>, + rng: &mut C::Rng, +) -> Result<(), SignError> +where + C: Configuration, + C::AssetValue: SubAssign, +{ + let authorization = authorization_for_default_spending_key::(accounts, parameters, rng); + let mut received_value = C::AssetValue::default(); + let mut reclaimed_value = senders + .iter() + .map(|sender| sender.asset().value) + .sum::(); + if reclaimed_value >= *change { + received_value += change.clone(); + reclaimed_value -= received_value.clone(); + *change = Default::default(); + } else { + received_value += reclaimed_value.clone(); + *change -= reclaimed_value; + reclaimed_value = Default::default(); + } + let receiver = default_receiver::( + accounts, + parameters, + Asset::::new(asset_id.clone(), received_value), + rng, + ); + posts.push(build_post( + Some(accounts), + utxo_accumulator.model(), + parameters, + &proving_context.to_public, + ToPublic::build( + authorization, + senders, + [receiver], + Asset::::new(asset_id.clone(), reclaimed_value), + ), + sink_accounts, + rng, + )?); + Ok(()) +} + /// Returns the [`Address`] corresponding to `authorization_context`. #[inline] pub fn address( @@ -935,6 +1067,7 @@ fn sign_withdraw( ) -> Result, SignError> where C: Configuration, + C::AssetValue: SubAssign, { let selection = select(accounts, assets, ¶meters.parameters, &asset, rng)?; sign_after_selection( @@ -962,6 +1095,7 @@ fn consolidate_internal( ) -> Result, SignError> where C: Configuration, + C::AssetValue: SubAssign, C::Identifier: PartialEq, { let asset = request.asset(); @@ -979,18 +1113,16 @@ where ) } -/// Signs a withdraw transaction for `asset` sent to `address`, where `selection` -/// owns at least `asset`. +/// Signs a private transfer of `asset` to `address`. #[allow(clippy::too_many_arguments)] #[inline] -fn sign_after_selection( +fn sign_after_selection_private_transfer( parameters: &SignerParameters, accounts: &AccountTable, assets: &C::AssetMap, utxo_accumulator: &mut C::UtxoAccumulator, asset: Asset, - address: Option>, - sink_accounts: Vec, + address: Address, selection: Selection, rng: &mut C::Rng, ) -> Result, SignError> @@ -1017,37 +1149,68 @@ where ); let authorization = authorization_for_default_spending_key::(accounts, ¶meters.parameters, rng); - let final_post = match address { - Some(address) => { - let receiver = receiver::( - ¶meters.parameters, - address, - asset, - Default::default(), - rng, - ); - build_post( - Some(accounts), - utxo_accumulator.model(), - ¶meters.parameters, - ¶meters.proving_context.private_transfer, - PrivateTransfer::build(authorization, senders, [change, receiver]), - Vec::new(), - rng, - )? - } - _ => build_post( - Some(accounts), - utxo_accumulator.model(), + let receiver = receiver::( + ¶meters.parameters, + address, + asset, + Default::default(), + rng, + ); + let final_post = build_post( + Some(accounts), + utxo_accumulator.model(), + ¶meters.parameters, + ¶meters.proving_context.private_transfer, + PrivateTransfer::build(authorization, senders, [change, receiver]), + Vec::new(), + rng, + )?; + posts.push(final_post); + Ok(SignResponse::new(posts)) +} + +/// Signs a withdraw transaction for `asset` sent to `address`, where `selection` +/// owns at least `asset`. +#[allow(clippy::too_many_arguments)] +#[inline] +fn sign_after_selection( + parameters: &SignerParameters, + accounts: &AccountTable, + assets: &C::AssetMap, + utxo_accumulator: &mut C::UtxoAccumulator, + asset: Asset, + address: Option>, + sink_accounts: Vec, + selection: Selection, + rng: &mut C::Rng, +) -> Result, SignError> +where + C: Configuration, + C::AssetValue: SubAssign, +{ + match address { + Some(address) => sign_after_selection_private_transfer( + parameters, + accounts, + assets, + utxo_accumulator, + asset, + address, + selection, + rng, + ), + _ => compute_to_public_transaction( + accounts, + assets, ¶meters.parameters, - ¶meters.proving_context.to_public, - ToPublic::build(authorization, senders, [change], asset), + ¶meters.proving_context, + &asset.id, sink_accounts, + selection, + utxo_accumulator, rng, - )?, - }; - posts.push(final_post); - Ok(SignResponse::new(posts)) + ), + } } /// Signs the `transaction`, generating transfer posts without releasing resources. @@ -1063,6 +1226,7 @@ fn sign_internal( ) -> Result, SignError> where C: Configuration, + C::AssetValue: SubAssign, { match transaction { Transaction::ToPrivate(asset) => { @@ -1118,6 +1282,7 @@ pub fn sign( ) -> Result, SignError> where C: Configuration, + C::AssetValue: SubAssign, { let result = sign_internal( parameters, @@ -1145,6 +1310,7 @@ pub fn consolidate( ) -> Result, SignError> where C: Configuration, + C::AssetValue: SubAssign, C::Identifier: PartialEq, { let result = consolidate_internal( @@ -1270,6 +1436,7 @@ pub fn sign_with_transaction_data( ) -> SignWithTransactionDataResult where C: Configuration, + C::AssetValue: SubAssign, TransferPost: Clone, { Ok(SignWithTransactionDataResponse( diff --git a/manta-accounting/src/wallet/signer/mod.rs b/manta-accounting/src/wallet/signer/mod.rs index e42e4e552..fafcca854 100644 --- a/manta-accounting/src/wallet/signer/mod.rs +++ b/manta-accounting/src/wallet/signer/mod.rs @@ -38,7 +38,7 @@ use crate::{ wallet::ledger::{self, Data}, }; use alloc::{boxed::Box, vec::Vec}; -use core::{cmp::max, convert::Infallible, fmt::Debug, hash::Hash}; +use core::{cmp::max, convert::Infallible, fmt::Debug, hash::Hash, ops::SubAssign}; use manta_crypto::{ accumulator::{ Accumulator, BatchInsertion, ExactSizeAccumulator, FromItemsAndWitnesses, ItemHashFunction, @@ -1640,7 +1640,10 @@ where /// Signs the `transaction`, generating transfer posts. #[inline] - pub fn sign(&mut self, transaction: Transaction) -> Result, SignError> { + pub fn sign(&mut self, transaction: Transaction) -> Result, SignError> + where + C::AssetValue: SubAssign, + { functions::sign( &self.parameters, self.state.accounts.as_ref(), @@ -1664,6 +1667,7 @@ where request: ConsolidationPrerequest, ) -> Result, SignError> where + C::AssetValue: SubAssign, C::Identifier: PartialEq, { functions::consolidate( @@ -1741,6 +1745,7 @@ where transaction: Transaction, ) -> Result, SignError> where + C::AssetValue: SubAssign, TransferPost: Clone, { functions::sign_with_transaction_data( @@ -1835,8 +1840,8 @@ where max(self.state.assets.select(asset).values.len() - 1, 1) } Transaction::ToPublic(asset, _) => { - max(self.state.assets.select(asset).values.len() - 1, 1) - } // note: change the estimation once we implement the topublic optimization + (self.state.assets.select(asset).values.len() + 1) / 2 + } } } } @@ -1844,7 +1849,8 @@ where impl Connection for Signer where C: Configuration, - C::AssetValue: CheckedAdd + CheckedSub, + C::AssetValue: + CheckedAdd + CheckedSub + SubAssign, C::Identifier: PartialEq, { type AssetMetadata = C::AssetMetadata; diff --git a/manta-pay/src/test/signer.rs b/manta-pay/src/test/signer.rs index 9b464fe50..62a76c1d2 100644 --- a/manta-pay/src/test/signer.rs +++ b/manta-pay/src/test/signer.rs @@ -32,7 +32,7 @@ use crate::{ use alloc::sync::Arc; use manta_accounting::{ transfer::{canonical::Transaction, IdentifiedAsset, Identifier}, - wallet::{signer::ConsolidationPrerequest, Wallet}, + wallet::{signer::ConsolidationPrerequest, test::PublicBalanceOracle, Wallet}, }; use manta_crypto::{ algebra::HasGenerator, @@ -304,8 +304,8 @@ async fn consolidation_test() { wallet.load_initial_state().await.expect("Sync error"); wallet.sync().await.expect("Sync error"); // 2) mint several UTXOs. - const NUMBER_OF_PRIVATE_UTXOS: usize = 9; - for _ in 0..NUMBER_OF_PRIVATE_UTXOS { + let number_of_private_utxos = rng.gen_range(0..30); + for _ in 0..number_of_private_utxos { let to_mint = rng.gen_range(Default::default()..public_balance); public_balance -= to_mint; let to_private = Transaction::::ToPrivate(Asset::new(asset_id.into(), to_mint)); @@ -320,7 +320,7 @@ async fn consolidation_test() { let balance_before_consolidation = wallet.balance(&asset_id.into()); assert_eq!( asset_list.len(), - NUMBER_OF_PRIVATE_UTXOS, + number_of_private_utxos, "The number of UTXOs in the asset list must be equal to the number of UTXOs minted." ); wallet @@ -341,3 +341,77 @@ async fn consolidation_test() { "The number of UTXOs after consolidation must be 1" ); } + +/// Tests the new to public implementation works as expected. +#[ignore] // We don't run this test on the CI because it takes a long time to run. +#[tokio::test] +async fn to_public_test() { + let mut rng = OsRng; + let asset_id = 8; + let ledger = load_ledger().expect("Error loading ledger"); + let account_id = rng.gen(); + let mut public_balance = rng.gen_range(Default::default()..u32::MAX as u128); + let mut wallet = create_new_wallet( + account_id, + public_balance, + asset_id, + ledger.clone(), + rng.gen(), + ) + .await; + let mut private_balance = 0; + // 1) reset the wallet and sync. + wallet.reset_state(); + wallet.load_initial_state().await.expect("Sync error"); + wallet.sync().await.expect("Sync error"); + // 2) mint several UTXOs and sync. + let number_of_private_utxos = rng.gen_range(0..30); + for _ in 0..number_of_private_utxos { + let to_mint = rng.gen_range(Default::default()..public_balance); + public_balance -= to_mint; + private_balance += to_mint; + let to_private = Transaction::::ToPrivate(Asset::new(asset_id.into(), to_mint)); + wallet + .post(to_private, Default::default()) + .await + .expect("Error posting ToPrivate"); + } + wallet.sync().await.expect("Sync error"); + // 3) to public some amount and estimate the number of posts. + let reclaim = rng.gen_range(Default::default()..private_balance); + public_balance += reclaim; + private_balance -= reclaim; + let to_public = + Transaction::::ToPublic(Asset::new(asset_id.into(), reclaim), account_id); + let estimation = wallet.signer().estimate_transferposts(&to_public); + let response = wallet + .sign(to_public, Default::default()) + .await + .expect("ToPublic error"); + assert_eq!( + response.posts.len(), + estimation, + "The estimation is different than the actual number of posts" + ); + wallet + .post(to_public, Default::default()) + .await + .expect("To Public error"); + // 4) check the balances are preserved + wallet.sync().await.expect("Sync error"); + assert_eq!( + wallet.balance(&asset_id.into()), + private_balance, + "Private balance not preserved." + ); + let real_public_balance = wallet + .ledger() + .public_balances() + .await + .expect("No public balances registered in the ledger") + .value(&asset_id.into()); + assert_eq!( + real_public_balance, public_balance, + "Public balance not preserved" + ); +}