diff --git a/CHANGELOG.md b/CHANGELOG.md index 40484dd33..c5ea5e4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added +- [\#345](https://github.com/Manta-Network/manta-rs/pull/345) Precompute ledger and minor bug fix. ### Changed diff --git a/manta-accounting/src/transfer/canonical.rs b/manta-accounting/src/transfer/canonical.rs index 9d321c19d..38d8876e2 100644 --- a/manta-accounting/src/transfer/canonical.rs +++ b/manta-accounting/src/transfer/canonical.rs @@ -30,7 +30,12 @@ use crate::{ }; use alloc::vec::Vec; use core::{fmt::Debug, hash::Hash}; -use manta_crypto::rand::{CryptoRng, RngCore}; +use manta_crypto::{ + arkworks::serialize::{ + CanonicalDeserialize, CanonicalSerialize, Read, SerializationError, Write, + }, + rand::{CryptoRng, RngCore}, +}; use manta_util::{create_seal, seal}; #[cfg(feature = "serde")] @@ -619,6 +624,101 @@ where pub to_public: VerifyingContext, } +impl CanonicalSerialize for MultiVerifyingContext +where + C: Configuration + ?Sized, + VerifyingContext: CanonicalSerialize, +{ + #[inline] + fn serialize(&self, mut writer: W) -> Result<(), SerializationError> + where + W: Write, + { + self.to_private.serialize(&mut writer)?; + self.private_transfer.serialize(&mut writer)?; + self.to_public.serialize(&mut writer)?; + Ok(()) + } + + #[inline] + fn serialized_size(&self) -> usize { + self.to_private.serialized_size() + + self.private_transfer.serialized_size() + + self.to_public.serialized_size() + } + + #[inline] + fn serialize_uncompressed(&self, mut writer: W) -> Result<(), SerializationError> + where + W: Write, + { + self.to_private.serialize_uncompressed(&mut writer)?; + self.private_transfer.serialize_uncompressed(&mut writer)?; + self.to_public.serialize_uncompressed(&mut writer)?; + Ok(()) + } + + #[inline] + fn serialize_unchecked(&self, mut writer: W) -> Result<(), SerializationError> + where + W: Write, + { + self.to_private.serialize_unchecked(&mut writer)?; + self.private_transfer.serialize_unchecked(&mut writer)?; + self.to_public.serialize_unchecked(&mut writer)?; + Ok(()) + } + + #[inline] + fn uncompressed_size(&self) -> usize { + self.to_private.uncompressed_size() + + self.private_transfer.uncompressed_size() + + self.to_public.uncompressed_size() + } +} + +impl CanonicalDeserialize for MultiVerifyingContext +where + C: Configuration + ?Sized, + VerifyingContext: CanonicalDeserialize, +{ + #[inline] + fn deserialize(mut reader: R) -> Result + where + R: Read, + { + Ok(Self { + to_private: CanonicalDeserialize::deserialize(&mut reader)?, + private_transfer: CanonicalDeserialize::deserialize(&mut reader)?, + to_public: CanonicalDeserialize::deserialize(&mut reader)?, + }) + } + + #[inline] + fn deserialize_uncompressed(mut reader: R) -> Result + where + R: Read, + { + Ok(Self { + to_private: CanonicalDeserialize::deserialize_uncompressed(&mut reader)?, + private_transfer: CanonicalDeserialize::deserialize_uncompressed(&mut reader)?, + to_public: CanonicalDeserialize::deserialize_uncompressed(&mut reader)?, + }) + } + + #[inline] + fn deserialize_unchecked(mut reader: R) -> Result + where + R: Read, + { + Ok(Self { + to_private: CanonicalDeserialize::deserialize_unchecked(&mut reader)?, + private_transfer: CanonicalDeserialize::deserialize_unchecked(&mut reader)?, + to_public: CanonicalDeserialize::deserialize_unchecked(&mut reader)?, + }) + } +} + impl MultiVerifyingContext where C: Configuration + ?Sized, diff --git a/manta-accounting/src/transfer/receiver.rs b/manta-accounting/src/transfer/receiver.rs index edb3b2557..fd2340f4a 100644 --- a/manta-accounting/src/transfer/receiver.rs +++ b/manta-accounting/src/transfer/receiver.rs @@ -466,3 +466,41 @@ where P::extend(input, &self.note); } } + +/// Unsafe Receiver Ledger +/// +/// # Safety +/// +/// This unsafe version of the receiver ledger does not perform the +/// [`is_not_registered`](ReceiverLedger::is_not_registered) check before registering a Utxo. +/// Therefore, it must only be used for testing purposes and with trusted inputs. +#[cfg(feature = "test")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "test")))] +pub mod unsafe_receiver_ledger { + use crate::transfer::{ + receiver::{ReceiverLedger, ReceiverPost, ReceiverPostingKey}, + utxo::Mint, + }; + + /// Unsafe Receiver Ledger + pub trait UnsafeReceiverLedger: ReceiverLedger + where + M: Mint, + { + /// Converts `utxo` into a [`ValidUtxo`](ReceiverLedger::ValidUtxo) without checking + /// it isn't already registered in `self`. + fn dont_check_registration(&self, utxo: M::Utxo) -> Self::ValidUtxo; + + /// Converts `receiver_post` into a [`ReceiverPostingKey`] without performing + /// the [`is_not_registered`](ReceiverLedger::is_not_registered) check. + fn dont_validate_receiver_post( + &self, + receiver_post: ReceiverPost, + ) -> ReceiverPostingKey { + ReceiverPostingKey { + utxo: self.dont_check_registration(receiver_post.utxo), + note: receiver_post.note, + } + } + } +} diff --git a/manta-accounting/src/transfer/sender.rs b/manta-accounting/src/transfer/sender.rs index 9a741e39c..a8e9cd984 100644 --- a/manta-accounting/src/transfer/sender.rs +++ b/manta-accounting/src/transfer/sender.rs @@ -780,3 +780,57 @@ where P::extend(input, self.nullifier.as_ref()); } } + +/// Unsafe Sender Ledger +/// +/// # Safety +/// +/// This unsafe version of the sender ledger does not perform the +/// [`has_matching_utxo_accumulator_output`] nor the [`is_unspent`] checks before spending an asset. +/// Therefore, it must only be used for testing purposes and with trusted inputs. +/// +/// [`has_matching_utxo_accumulator_output`]: SenderLedger::has_matching_utxo_accumulator_output +/// [`is_unspent`]: SenderLedger::is_unspent +#[cfg(feature = "test")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "test")))] +pub mod unsafe_sender_ledger { + use crate::transfer::{ + sender::{SenderLedger, SenderPost, SenderPostingKey}, + utxo::{Spend, UtxoAccumulatorOutput}, + }; + + /// Unsafe Sender Ledger + pub trait UnsafeSenderLedger: SenderLedger + where + S: Spend, + { + /// Transforms `output` into a [`ValidUtxoAccumulatorOutput`] without checking + /// it coincides with one of the [`UtxoAccumulatorOutput`]s in `self`. + /// + /// [`ValidUtxoAccumulatorOutput`]: SenderLedger::ValidUtxoAccumulatorOutput + fn dont_check_utxo_accumulator_output( + &self, + output: UtxoAccumulatorOutput, + ) -> Self::ValidUtxoAccumulatorOutput; + + /// Transforms `nullifier` into a [`ValidNullifier`](SenderLedger::ValidNullifier) + /// without checking that `nullifier` isn't registered in `self` already. + fn dont_check_nullifier(&self, nullifier: S::Nullifier) -> Self::ValidNullifier; + + /// Transforms `sender_post` into a [`SenderPostingKey`] without running the + /// [`has_matching_utxo_accumulator_output`] nor the [`is_unspent`] checks before. + /// + /// [`has_matching_utxo_accumulator_output`]: SenderLedger::has_matching_utxo_accumulator_output + /// [`is_unspent`]: SenderLedger::is_unspent + fn dont_validate_sender_post( + &self, + sender_post: SenderPost, + ) -> SenderPostingKey { + SenderPostingKey:: { + utxo_accumulator_output: self + .dont_check_utxo_accumulator_output(sender_post.utxo_accumulator_output), + nullifier: self.dont_check_nullifier(sender_post.nullifier), + } + } + } +} diff --git a/manta-accounting/src/transfer/test.rs b/manta-accounting/src/transfer/test/mod.rs similarity index 99% rename from manta-accounting/src/transfer/test.rs rename to manta-accounting/src/transfer/test/mod.rs index f7588138f..aa256873d 100644 --- a/manta-accounting/src/transfer/test.rs +++ b/manta-accounting/src/transfer/test/mod.rs @@ -35,6 +35,8 @@ use manta_crypto::{ }; use manta_util::into_array_unchecked; +pub mod unverified_transfers; + /// Samples a distribution over `count`-many values summing to `total`. /// /// # Warning diff --git a/manta-accounting/src/transfer/test/unverified_transfers.rs b/manta-accounting/src/transfer/test/unverified_transfers.rs new file mode 100644 index 000000000..a31ad81f6 --- /dev/null +++ b/manta-accounting/src/transfer/test/unverified_transfers.rs @@ -0,0 +1,575 @@ +// 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 . +//! Unverified Transfers +//! +//! # SAFETY +//! +//! This module is for testing purposes only. [`UnsafeTransferPost`] and [`UnsafeTransferPostBody`] do not +//! contain a proof nor a signature. Because of that, they can only be used with trusted inputs. + +use crate::transfer::{ + Asset, AuthorizationSignature, Configuration, FullParametersRef, Parameters, Proof, ProofInput, + ProvingContext, Receiver, ReceiverPost, Sender, SenderPost, SpendingKey, Transfer, + TransferLedger, TransferLedgerSuperPostingKey, TransferPost, TransferPostBody, + TransferPostingKey, TransferPostingKeyRef, +}; +use alloc::vec::Vec; +use core::{fmt::Debug, hash::Hash}; +use manta_crypto::{ + constraint::{HasInput, Input}, + rand::{CryptoRng, RngCore}, +}; +use manta_util::codec::{Encode, Write}; + +#[cfg(feature = "serde")] +use manta_util::serde::{Deserialize, Serialize}; + +pub use crate::transfer::{ + receiver::unsafe_receiver_ledger::UnsafeReceiverLedger, + sender::unsafe_sender_ledger::UnsafeSenderLedger, +}; + +/// Unsafe Transfer Post Body +#[cfg_attr( + feature = "serde", + derive(Deserialize, Serialize), + serde( + bound( + deserialize = r" + C::AssetId: Deserialize<'de>, + C::AssetValue: Deserialize<'de>, + SenderPost: Deserialize<'de>, + ReceiverPost: Deserialize<'de>, + ", + serialize = r" + C::AssetId: Serialize, + C::AssetValue: Serialize, + SenderPost: Serialize, + ReceiverPost: Serialize, + ", + ), + crate = "manta_util::serde", + deny_unknown_fields + ) +)] +#[derive(derivative::Derivative)] +#[derivative( + Clone(bound = r" + C::AssetId: Clone, + C::AssetValue: Clone, + SenderPost: Clone, + ReceiverPost: Clone, + "), + Debug(bound = r" + C::AssetId: Debug, + C::AssetValue: Debug, + SenderPost: Debug, + ReceiverPost: Debug, + "), + Eq(bound = r" + C::AssetId: Eq, + C::AssetValue: Eq, + SenderPost: Eq, + ReceiverPost: Eq, + "), + Hash(bound = r" + C::AssetId: Hash, + C::AssetValue: Hash, + SenderPost: Hash, + ReceiverPost: Hash, + "), + PartialEq(bound = r" + C::AssetId: PartialEq, + C::AssetValue: PartialEq, + SenderPost: PartialEq, + ReceiverPost: PartialEq, + ") +)] +pub struct UnsafeTransferPostBody +where + C: Configuration + ?Sized, +{ + /// Asset Id + pub asset_id: Option, + + /// Sources + pub sources: Vec, + + /// Sender Posts + pub sender_posts: Vec>, + + /// Receiver Posts + pub receiver_posts: Vec>, + + /// Sinks + pub sinks: Vec, +} + +impl UnsafeTransferPostBody +where + C: Configuration + ?Sized, +{ + /// Builds a new [`UnsafeTransferPostBody`]. + #[inline] + fn build< + const SOURCES: usize, + const SENDERS: usize, + const RECEIVERS: usize, + const SINKS: usize, + >( + asset_id: Option, + sources: [C::AssetValue; SOURCES], + senders: [Sender; SENDERS], + receivers: [Receiver; RECEIVERS], + sinks: [C::AssetValue; SINKS], + ) -> Self { + Self { + asset_id, + sources: sources.into(), + sender_posts: senders.into_iter().map(Sender::::into_post).collect(), + receiver_posts: receivers + .into_iter() + .map(Receiver::::into_post) + .collect(), + sinks: sinks.into(), + } + } + + /// Constructs an [`Asset`] against the `asset_id` of `self` and `value`. + #[inline] + fn construct_asset(&self, value: &C::AssetValue) -> Option> { + Some(Asset::::new(self.asset_id.clone()?, value.clone())) + } + + /// Returns the `k`-th source in the transfer. + #[inline] + pub fn source(&self, k: usize) -> Option> { + self.sources + .get(k) + .and_then(|value| self.construct_asset(value)) + } + + /// Returns the `k`-th sink in the transfer. + #[inline] + pub fn sink(&self, k: usize) -> Option> { + self.sinks + .get(k) + .and_then(|value| self.construct_asset(value)) + } +} + +impl Encode for UnsafeTransferPostBody +where + C: Configuration + ?Sized, + C::AssetId: Encode, + C::AssetValue: Encode, + SenderPost: Encode, + ReceiverPost: Encode, +{ + #[inline] + fn encode(&self, mut writer: W) -> Result<(), W::Error> + where + W: Write, + { + self.asset_id.encode(&mut writer)?; + self.sources.encode(&mut writer)?; + self.sender_posts.encode(&mut writer)?; + self.receiver_posts.encode(&mut writer)?; + self.sinks.encode(&mut writer)?; + Ok(()) + } +} + +impl Input for UnsafeTransferPostBody +where + C: Configuration + ?Sized, +{ + #[inline] + fn extend(&self, input: &mut ProofInput) { + if let Some(asset_id) = &self.asset_id { + C::ProofSystem::extend(input, asset_id); + } + self.sources + .iter() + .for_each(|source| C::ProofSystem::extend(input, source)); + self.sender_posts + .iter() + .for_each(|post| C::ProofSystem::extend(input, post)); + self.receiver_posts + .iter() + .for_each(|post| C::ProofSystem::extend(input, post)); + self.sinks + .iter() + .for_each(|sink| C::ProofSystem::extend(input, sink)); + } +} + +impl From> for TransferPostBody +where + C: Configuration + ?Sized, + Proof: Default, +{ + fn from(unsafe_transfer_post_body: UnsafeTransferPostBody) -> Self { + Self { + asset_id: unsafe_transfer_post_body.asset_id, + sources: unsafe_transfer_post_body.sources, + sender_posts: unsafe_transfer_post_body.sender_posts, + receiver_posts: unsafe_transfer_post_body.receiver_posts, + sinks: unsafe_transfer_post_body.sinks, + proof: Default::default(), + } + } +} + +impl From> for UnsafeTransferPostBody +where + C: Configuration + ?Sized, +{ + fn from(transfer_post_body: TransferPostBody) -> Self { + Self { + asset_id: transfer_post_body.asset_id, + sources: transfer_post_body.sources, + sender_posts: transfer_post_body.sender_posts, + receiver_posts: transfer_post_body.receiver_posts, + sinks: transfer_post_body.sinks, + } + } +} + +/// Unsafe Transfer Post +#[cfg_attr( + feature = "serde", + derive(Deserialize, Serialize), + serde( + bound( + deserialize = r" + AuthorizationSignature: Deserialize<'de>, + UnsafeTransferPostBody: Deserialize<'de>, + C::AccountId: Deserialize<'de>, + ", + serialize = r" + AuthorizationSignature: Serialize, + UnsafeTransferPostBody: Serialize, + C::AccountId: Serialize, + ", + ), + crate = "manta_util::serde", + deny_unknown_fields + ) +)] +#[derive(derivative::Derivative)] +#[derivative( + Clone( + bound = "AuthorizationSignature: Clone, UnsafeTransferPostBody: Clone, C::AccountId: Clone" + ), + Debug( + bound = "AuthorizationSignature: Debug, UnsafeTransferPostBody: Debug, C::AccountId: Debug" + ), + Eq(bound = "AuthorizationSignature: Eq, UnsafeTransferPostBody: Eq, C::AccountId: Eq"), + Hash( + bound = "AuthorizationSignature: Hash, UnsafeTransferPostBody: Hash, C::AccountId: Hash" + ), + PartialEq( + bound = "AuthorizationSignature: PartialEq, UnsafeTransferPostBody: PartialEq, C::AccountId: PartialEq" + ) +)] +pub struct UnsafeTransferPost +where + C: Configuration + ?Sized, +{ + /// Unsafe Transfer Post Body + pub body: UnsafeTransferPostBody, + + /// Sink accounts + pub sink_accounts: Vec, +} + +impl UnsafeTransferPost +where + C: Configuration + ?Sized, +{ + /// Creates a new [`UnsafeTransferPost`] from `body` and `sink_accounts`. + #[inline] + fn new(body: UnsafeTransferPostBody, sink_accounts: Vec) -> Self { + Self { + body, + sink_accounts, + } + } + + /// Returns the `k`-th source in the transfer. + #[inline] + pub fn source(&self, k: usize) -> Option> { + self.body.source(k) + } + + /// Returns the `k`-th sink in the transfer. + #[inline] + pub fn sink(&self, k: usize) -> Option> { + self.body.sink(k) + } + + /// Generates the public input for the [`Transfer`] validation proof. + #[inline] + pub fn generate_proof_input(&self) -> ProofInput { + let mut input = Default::default(); + self.extend(&mut input); + input + } +} + +impl Encode for UnsafeTransferPost +where + C: Configuration + ?Sized, + UnsafeTransferPostBody: Encode, + C::AccountId: Encode, +{ + #[inline] + fn encode(&self, mut writer: W) -> Result<(), W::Error> + where + W: Write, + { + self.body.encode(&mut writer)?; + self.sink_accounts.encode(&mut writer)?; + Ok(()) + } +} + +impl Input for UnsafeTransferPost +where + C: Configuration + ?Sized, +{ + #[inline] + fn extend(&self, input: &mut ProofInput) { + self.body.extend(input); + } +} + +impl From> for TransferPost +where + C: Configuration + ?Sized, + Proof: Default, +{ + fn from(unsafe_transfer_post: UnsafeTransferPost) -> Self { + Self { + authorization_signature: None, + body: unsafe_transfer_post.body.into(), + sink_accounts: unsafe_transfer_post.sink_accounts, + } + } +} + +impl From> for UnsafeTransferPost +where + C: Configuration + ?Sized, +{ + fn from(transfer_post: TransferPost) -> Self { + Self { + body: transfer_post.body.into(), + sink_accounts: transfer_post.sink_accounts, + } + } +} + +impl + Transfer +where + C: Configuration, +{ + /// Converts `self` into its [`UnsafeTransferPost`]. + #[inline] + pub fn into_unsafe_post( + self, + parameters: FullParametersRef, + proving_context: &ProvingContext, + spending_key: Option<&SpendingKey>, + sink_accounts: Vec, + rng: &mut R, + ) -> UnsafeTransferPost + where + R: CryptoRng + RngCore + ?Sized, + { + let _ = spending_key; + UnsafeTransferPost::new( + self.into_unsafe_post_body(parameters, proving_context, rng), + sink_accounts, + ) + } + + /// Converts `self` into its [`UnsafeTransferPostBody`]. + #[inline] + pub fn into_unsafe_post_body( + self, + parameters: FullParametersRef, + proving_context: &ProvingContext, + rng: &mut R, + ) -> UnsafeTransferPostBody + where + R: CryptoRng + RngCore + ?Sized, + { + let _ = (parameters, proving_context, rng); + UnsafeTransferPostBody::build( + self.asset_id, + self.sources, + self.senders, + self.receivers, + self.sinks, + ) + } +} + +/// Unsafe Ledger +/// +/// # Safety +/// +/// This unsafe version of the transfer ledger does not perform the +/// any checks before registering a transaction. +/// Therefore, it must only be used for testing purposes and with trusted inputs. +pub trait UnsafeLedger: + TransferLedger + + UnsafeReceiverLedger< + Parameters, + SuperPostingKey = (Self::ValidProof, TransferLedgerSuperPostingKey), + > + UnsafeSenderLedger< + Parameters, + SuperPostingKey = (Self::ValidProof, TransferLedgerSuperPostingKey), + > + Sized +where + C: Configuration + ?Sized, +{ + /// Transforms the accounts in `sources` into [`ValidSourceAccount`]s without checking + /// they have enough funds. + /// + /// [`ValidSourceAccount`]: TransferLedger::ValidSourceAccount + fn dont_check_source_accounts( + &self, + asset_id: &C::AssetId, + sources: I, + ) -> Vec + where + I: Iterator; + + /// Transforms the accounts in `sinks` into [`ValidSinkAccount`]s without checking + /// they exist. + /// + /// [`ValidSinkAccount`]: TransferLedger::ValidSinkAccount + fn dont_check_sink_accounts( + &self, + asset_id: &C::AssetId, + sinks: I, + ) -> Vec + where + I: Iterator; + + /// Runs [`dont_check_source_accounts`] and [`dont_check_sink_accounts`] on the sources + /// and sinks, respectively. + /// + /// [`dont_check_source_accounts`]: UnsafeLedger::dont_check_source_accounts + /// [`dont_check_sink_accounts`]: UnsafeLedger::dont_check_sink_accounts + fn dont_check_public_participants( + &self, + asset_id: &Option, + source_accounts: Vec, + source_values: Vec, + sink_accounts: Vec, + sink_values: Vec, + ) -> (Vec, Vec) { + let sources = source_values.len(); + let sinks = sink_values.len(); + let sources = if sources > 0 { + self.dont_check_source_accounts( + asset_id.as_ref().unwrap(), + source_accounts.into_iter().zip(source_values), + ) + } else { + Vec::new() + }; + let sinks = if sinks > 0 { + self.dont_check_sink_accounts( + asset_id.as_ref().unwrap(), + sink_accounts.into_iter().zip(sink_values), + ) + } else { + Vec::new() + }; + (sources, sinks) + } + + /// Converts the [`Proof`] in `posting_key` into a [`ValidProof`](TransferLedger::ValidProof) + /// without validating it. + fn dont_check_proof( + &self, + posting_key: TransferPostingKeyRef, + ) -> (Self::ValidProof, Self::Event); + + /// Converts `post`, `source_accounts` and `sink_accounts` into a [`TransferPostingKey`] + /// without running any checks, namely it doesn't: + /// 1) verify the [`AuthorizationSignature`] in `post` + /// 2) verify the [`Proof`] in `post` + /// 3) check the [`Nullifier`] in `post` hasn't been posted to `self` + /// 4) check the [`UtxoAccumulatorOutput`] in `post` coincides with one + /// of the [`UtxoAccumulatorOutput`]s in `self`. + /// 5) check the [`Utxo`] in `post` hasn't been already registered to `self`. + /// 6) check the public participants, that is, `source_accounts` and `sink_accounts`. + /// + /// [`Nullifier`]: crate::transfer::Nullifier + /// [`UtxoAccumulatorOutput`]: crate::transfer::UtxoAccumulatorOutput + /// [`Utxo`]: crate::transfer::Utxo + fn dont_validate( + &self, + post: TransferPost, + source_accounts: Vec, + sink_accounts: Vec, + ) -> TransferPostingKey { + let (source_posting_keys, sink_posting_keys) = self.dont_check_public_participants( + &post.body.asset_id, + source_accounts, + post.body.sources, + sink_accounts, + post.body.sinks, + ); + let sender_posting_keys = post + .body + .sender_posts + .into_iter() + .map(move |s| self.dont_validate_sender_post(s)) + .collect::>(); + let receiver_posting_keys = post + .body + .receiver_posts + .into_iter() + .map(move |r| self.dont_validate_receiver_post(r)) + .collect::>(); + let (proof, event) = self.dont_check_proof(TransferPostingKeyRef { + authorization_key: &post.authorization_signature.map(|s| s.authorization_key), + asset_id: &post.body.asset_id, + sources: &source_posting_keys, + senders: &sender_posting_keys, + receivers: &receiver_posting_keys, + sinks: &sink_posting_keys, + proof: post.body.proof, + }); + TransferPostingKey { + asset_id: post.body.asset_id, + source_posting_keys, + sender_posting_keys, + receiver_posting_keys, + sink_posting_keys, + proof, + event, + } + } +} diff --git a/manta-accounting/src/wallet/mod.rs b/manta-accounting/src/wallet/mod.rs index 06477e55e..5a27cab0d 100644 --- a/manta-accounting/src/wallet/mod.rs +++ b/manta-accounting/src/wallet/mod.rs @@ -476,6 +476,10 @@ where Err(SyncError::MissingProofAuthorizationKey) => { Err(Error::MissingProofAuthorizationKey) } + + Err(SyncError::InconsistentBalance) => { + Err(Error::Inconsistency(InconsistencyError::WalletBalance)) + } } } diff --git a/manta-accounting/src/wallet/signer/functions.rs b/manta-accounting/src/wallet/signer/functions.rs index 81dc175d8..f14a7f8ef 100644 --- a/manta-accounting/src/wallet/signer/functions.rs +++ b/manta-accounting/src/wallet/signer/functions.rs @@ -51,7 +51,12 @@ use manta_crypto::{ rand::Rand, }; use manta_util::{ - array_map, cmp::Independence, into_array_unchecked, iter::IteratorExt, persistence::Rollback, + array_map, + cmp::Independence, + into_array_unchecked, + iter::IteratorExt, + num::{CheckedAdd, CheckedSub}, + persistence::Rollback, vec::VecExt, }; @@ -221,10 +226,11 @@ fn sync_with( mut nullifiers: Vec>, is_partial: bool, rng: &mut C::Rng, -) -> SyncResponse +) -> Result, SyncError> where C: Configuration, I: Iterator, Note)>, + C::AssetValue: CheckedAdd + CheckedSub, { let nullifier_count = nullifiers.len(); let mut deposit = Vec::new(); @@ -272,7 +278,8 @@ where }); checkpoint.update_from_nullifiers(nullifier_count); checkpoint.update_from_utxo_accumulator(utxo_accumulator); - SyncResponse { + normalize_assets::(&mut deposit, &mut withdraw)?; + Ok(SyncResponse { checkpoint: checkpoint.clone(), balance_update: if is_partial { // TODO: Whenever we are doing a full update, don't even build the `deposit` and @@ -283,7 +290,89 @@ where assets: assets.assets().into(), } }, + }) +} + +/// Sums all the values with the same [`Asset`] id in `assets`. +fn sum_values(assets: &mut Vec>) -> Result<(), SyncError> +where + C: Configuration, + C::AssetValue: CheckedAdd, +{ + let mut result = Vec::<(_, C::AssetValue)>::new(); + + for asset in assets.iter() { + if let Some(entry) = result.iter_mut().find(|(id, _)| *id == asset.id) { + entry.1 = entry + .1 + .clone() + .checked_add(asset.value.clone()) + .ok_or(SyncError::InconsistentBalance)?; + } else { + result.push((asset.id.clone(), asset.value.clone())); + } + } + + *assets = result + .into_iter() + .map(|(id, value)| Asset::::new(id, value)) + .collect(); + Ok(()) +} + +/// First it runs [`sum_values`] in both `deposit` and `withdraw`. Then, for each [`Asset`] id +/// which happens in both `deposit` and `withdraw`: +/// 1) computes the difference `diff = asset_value(deposit) - asset_value(withdraw)` +/// 2) If `diff > 0`, it replaces the corresponding entry in `deposit` with `diff` and deletes +/// the entry in `withdraw`. +/// 3) If `diff < 0`, it replaces the corresponding entry in `withdraw` with `-diff` and deletes +/// the entry in `deposit`. +/// 4) If `diff = 0`, it deletes the entry in `deposit` and `withdraw`. +fn normalize_assets( + deposit: &mut Vec>, + withdraw: &mut Vec>, +) -> Result<(), SyncError> +where + C: Configuration, + C::AssetValue: CheckedAdd + CheckedSub, +{ + sum_values::(deposit)?; + sum_values::(withdraw)?; + let mut i = 0; + while i < deposit.len() { + let deposit_asset = deposit[i].clone(); + if let Some(withdraw_index) = withdraw + .iter() + .position(|asset| asset.id == deposit_asset.id) + { + let withdraw_asset = &mut withdraw[withdraw_index]; + if deposit_asset.value > withdraw_asset.value { + let diff = deposit_asset + .value + .clone() + .checked_sub(withdraw_asset.value.clone()) + .ok_or(SyncError::InconsistentBalance)?; + deposit[i].value = diff; + withdraw.remove(withdraw_index); + i += 1; + } else if deposit_asset.value < withdraw_asset.value { + let diff = withdraw_asset + .value + .clone() + .checked_sub(deposit_asset.value.clone()) + .ok_or(SyncError::InconsistentBalance)?; + withdraw[withdraw_index].value = diff; + deposit.remove(i); + } else { + deposit.remove(i); + withdraw.remove(withdraw_index); + i += 1; + } + } else { + i += 1; + } } + Ok(()) } /// Updates the internal ledger state, returning the new asset distribution. @@ -775,6 +864,7 @@ pub fn sync( ) -> Result, SyncError> where C: Configuration, + C::AssetValue: CheckedAdd + CheckedSub, { let ( has_pruned, @@ -795,7 +885,7 @@ where rng, ); utxo_accumulator.commit(); - Ok(response) + response } /// Signs a withdraw transaction for `asset` sent to `address`. diff --git a/manta-accounting/src/wallet/signer/mod.rs b/manta-accounting/src/wallet/signer/mod.rs index 75b38929f..42e2be2f7 100644 --- a/manta-accounting/src/wallet/signer/mod.rs +++ b/manta-accounting/src/wallet/signer/mod.rs @@ -46,7 +46,11 @@ use manta_crypto::{ }, rand::{CryptoRng, FromEntropy, RngCore}, }; -use manta_util::{future::LocalBoxFutureResult, persistence::Rollback}; +use manta_util::{ + future::LocalBoxFutureResult, + num::{CheckedAdd, CheckedSub}, + persistence::Rollback, +}; #[cfg(feature = "serde")] use manta_util::serde::{Deserialize, Serialize}; @@ -640,6 +644,9 @@ where /// Missing Proof Authorization Key MissingProofAuthorizationKey, + + /// Inconsistent Balance + InconsistentBalance, } /// Synchronization Result @@ -1319,7 +1326,10 @@ where pub fn sync( &mut self, request: SyncRequest, - ) -> Result, SyncError> { + ) -> Result, SyncError> + where + C::AssetValue: CheckedAdd + CheckedSub, + { functions::sync( &self.parameters, self.state @@ -1533,6 +1543,7 @@ where impl Connection for Signer where C: Configuration, + C::AssetValue: CheckedAdd + CheckedSub, { type AssetMetadata = C::AssetMetadata; type Checkpoint = C::Checkpoint; diff --git a/manta-crypto/src/arkworks/groth16.rs b/manta-crypto/src/arkworks/groth16.rs index 97518e4fb..e23c97e1b 100644 --- a/manta-crypto/src/arkworks/groth16.rs +++ b/manta-crypto/src/arkworks/groth16.rs @@ -71,7 +71,7 @@ pub struct Error; ) )] #[derive(derivative::Derivative)] -#[derivative(Clone, Debug, Default, Eq, PartialEq)] +#[derivative(Clone, Debug, Default(bound = ""), Eq, PartialEq)] pub struct Proof( /// Groth16 Proof #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_proof::"))] diff --git a/manta-crypto/src/merkle_tree/single_path.rs b/manta-crypto/src/merkle_tree/single_path.rs index eded49459..78b921552 100644 --- a/manta-crypto/src/merkle_tree/single_path.rs +++ b/manta-crypto/src/merkle_tree/single_path.rs @@ -26,6 +26,9 @@ use crate::merkle_tree::{ }; use core::{fmt::Debug, hash::Hash}; +#[cfg(feature = "serde")] +use manta_util::serde::{Deserialize, Serialize}; + /// Tree Length State #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Length { @@ -43,6 +46,18 @@ pub enum Length { pub type SinglePathMerkleTree = MerkleTree>; /// Single Path Merkle Tree Backing Structure +#[cfg_attr( + feature = "serde", + derive(Deserialize, Serialize), + serde( + bound( + deserialize = "LeafDigest: Deserialize<'de>, InnerDigest: Deserialize<'de>", + serialize = "LeafDigest: Serialize, InnerDigest: Serialize", + ), + crate = "manta_util::serde", + deny_unknown_fields + ) +)] #[derive(derivative::Derivative)] #[derivative( Clone(bound = "LeafDigest: Clone, InnerDigest: Clone"), diff --git a/manta-pay/Cargo.toml b/manta-pay/Cargo.toml index 44f34eb21..9fb9e22d3 100644 --- a/manta-pay/Cargo.toml +++ b/manta-pay/Cargo.toml @@ -26,12 +26,16 @@ maintenance = { status = "actively-developed" } [[bin]] name = "generate_parameters" -required-features = ["groth16", "manta-util/std", "parameters", "serde"] +required-features = ["manta-util/std", "parameters", "serde"] [[bin]] name = "simulation" required-features = ["clap", "groth16", "simulation"] +[[bin]] +name = "precompute_ledger" +required-features = ["download", "parameters", "simulation", "serde"] + [features] # Enable Arkworks Backend arkworks = [ @@ -65,7 +69,7 @@ scale = ["scale-codec", "scale-info"] scale-std = ["scale", "scale-codec/std", "scale-info/std", "std"] # Serde -serde = ["manta-accounting/serde", "manta-crypto/serde"] +serde = ["bincode", "manta-accounting/serde", "manta-crypto/serde"] # Simulation Framework simulation = [ @@ -109,6 +113,7 @@ websocket = [ [dependencies] aes-gcm = { version = "0.9.4", default-features = false, features = ["aes", "alloc"] } +bincode = { version = "1.3.3", optional = true, default-features = false } bip0039 = { version = "0.10.1", optional = true, default-features = false } bip32 = { version = "0.4.0", optional = true, default-features = false, features = ["bip39", "secp256k1"] } blake2 = { version = "0.10.6", default-features = false } @@ -116,7 +121,7 @@ bs58 = { version = "0.4.0", optional = true, default-features = false, features clap = { version = "4.1.8", optional = true, default-features = false, features = ["color", "derive", "std", "suggestions", "unicode", "wrap_help"] } derivative = { version = "2.2.0", default-features = false, features = ["use_core"] } futures = { version = "0.3.25", optional = true, default-features = false } -indexmap = { version = "1.9.2", optional = true, default-features = false } +indexmap = { version = "1.9.2", optional = true, default-features = false, features = ["serde"] } manta-accounting = { path = "../manta-accounting", default-features = false } manta-crypto = { path = "../manta-crypto", default-features = false, features = ["rand_chacha"] } manta-parameters = { path = "../manta-parameters", optional = true, default-features = false } @@ -132,6 +137,5 @@ tokio-tungstenite = { version = "0.18.0", optional = true, default-features = fa ws_stream_wasm = { version = "0.7.3", optional = true, default-features = false } [dev-dependencies] -bincode = { version = "1.3.3", default-features = false } manta-crypto = { path = "../manta-crypto", default-features = false, features = ["getrandom"] } manta-pay = { path = ".", default-features = false, features = ["download", "parameters", "groth16", "scale", "scale-std", "serde", "serde_json", "std", "test", "wallet"] } diff --git a/manta-pay/src/bin/precompute_ledger.rs b/manta-pay/src/bin/precompute_ledger.rs new file mode 100644 index 000000000..23a98fe8e --- /dev/null +++ b/manta-pay/src/bin/precompute_ledger.rs @@ -0,0 +1,127 @@ +// 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 . + +//! Precompute ledger + +extern crate alloc; + +use alloc::sync::Arc; +use manta_crypto::rand::{ChaCha20Rng, SeedableRng}; +use manta_pay::{ + parameters::load_parameters, + simulation::ledger::{safe_fill_ledger, unsafe_fill_ledger, Ledger}, +}; +use std::{ + env, + fs::{self, OpenOptions}, + io::{self}, + path::PathBuf, + str::FromStr, +}; +use tokio::{runtime::Runtime, sync::RwLock}; + +/// Mode +#[derive(Debug)] +enum Mode { + /// Safe mode + Safe, + + /// Unsafe mode + Unsafe, +} + +impl FromStr for Mode { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "UNSAFE" => Ok(Mode::Unsafe), + "SAFE" => Ok(Mode::Safe), + _ => Err(()), + } + } +} + +/// Default number of coins +const NUMBER_OF_COINS: usize = 10000; + +/// Default Mode +const DEFAULT_MODE: Mode = Mode::Unsafe; + +/// Builds sample transactions on a ledger for testing purposes. +/// +/// # Instructions +/// +/// To run this binary file, use the following command +/// - cargo run --release --package manta-pay --bin precompute_ledger +/// --all-features -- `directory` `NUMBER_OF_COINS` `MODE` +#[inline] +fn main() -> io::Result<()> { + let target_dir = env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or(env::current_dir()?); + assert!( + target_dir.is_dir() || !target_dir.exists(), + "Specify a directory to place the generated files: {target_dir:?}.", + ); + fs::create_dir_all(&target_dir)?; + let number_of_coins = env::args() + .nth(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(NUMBER_OF_COINS); + let mode = env::args() + .nth(3) + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_MODE); + let target_file = OpenOptions::new() + .create_new(true) + .write(true) + .open(target_dir.join("precomputed_ledger"))?; + let directory = tempfile::tempdir().expect("Unable to generate temporary test directory."); + let mut rng = ChaCha20Rng::from_seed([0; 32]); + let (proving_context, verifying_context, parameters, utxo_accumulator_model) = + load_parameters(directory.path()).expect("Unable to load parameters."); + let asset_id = 8.into(); + let ledger = Arc::new(RwLock::new(Ledger::new( + utxo_accumulator_model.clone(), + verifying_context, + parameters.clone(), + ))); + let runtime = Runtime::new().expect("Unable to start tokio runtime"); + match mode { + Mode::Safe => runtime.block_on(safe_fill_ledger( + number_of_coins, + &ledger, + &proving_context, + ¶meters, + &utxo_accumulator_model, + asset_id, + &mut rng, + )), + Mode::Unsafe => runtime.block_on(unsafe_fill_ledger( + number_of_coins, + &ledger, + &proving_context, + ¶meters, + &utxo_accumulator_model, + asset_id, + &mut rng, + )), + }; + runtime.block_on(async { ledger.read().await.serialize_into(target_file) }); + directory.close() +} diff --git a/manta-pay/src/bin/simulation.rs b/manta-pay/src/bin/simulation.rs index 1d43790a9..962a39c33 100644 --- a/manta-pay/src/bin/simulation.rs +++ b/manta-pay/src/bin/simulation.rs @@ -21,6 +21,12 @@ use manta_crypto::rand::OsRng; use manta_pay::{parameters::load_parameters, simulation::Simulation}; /// Runs the Manta Pay simulation. +/// +/// # Instructions +/// +/// To run this binary file, use the following command +/// - cargo run --release --package manta-pay --all-features --bin simulation `number_of_actors` +/// `number_of_steps` `number_of_asset_ids` `initial_balance` pub fn main() { let simulation = Simulation::parse(); let mut rng = OsRng; diff --git a/manta-pay/src/simulation/ledger/mod.rs b/manta-pay/src/simulation/ledger/mod.rs index 48e4c242c..a2f207451 100644 --- a/manta-pay/src/simulation/ledger/mod.rs +++ b/manta-pay/src/simulation/ledger/mod.rs @@ -24,10 +24,17 @@ use crate::{ utxo::{ AssetId, AssetValue, Checkpoint, FullIncomingNote, MerkleTreeConfiguration, Parameters, }, - AccountId, Config, MultiVerifyingContext, Nullifier, ProofSystem, TransferPost, Utxo, - UtxoAccumulatorModel, + AccountId, Config, MultiProvingContext, MultiVerifyingContext, Nullifier, ProofSystem, + TransferPost, Utxo, UtxoAccumulatorModel, }, signer::InitialSyncData, + test::payment::{ + private_transfer::prove_full as private_transfer, to_private::prove_full as to_private, + to_public::prove_full as to_public, + unsafe_private_transfer::unsafe_no_prove_full as unsafe_private_transfer, + unsafe_to_private::unsafe_no_prove_full as unsafe_to_private, + unsafe_to_public::unsafe_no_prove_full as unsafe_to_public, UtxoAccumulator, + }, }; use alloc::{sync::Arc, vec::Vec}; use core::convert::Infallible; @@ -38,6 +45,7 @@ use manta_accounting::{ canonical::TransferShape, receiver::{ReceiverLedger, ReceiverPostError}, sender::{SenderLedger, SenderPostError}, + test::unverified_transfers::{UnsafeLedger, UnsafeReceiverLedger, UnsafeSenderLedger}, InvalidAuthorizationSignature, InvalidSinkAccount, InvalidSourceAccount, SinkPostingKey, SourcePostingKey, TransferLedger, TransferLedgerSuperPostingKey, TransferPostError, TransferPostingKeyRef, UtxoAccumulatorOutput, @@ -55,13 +63,17 @@ use manta_crypto::{ self, forest::{Configuration, FixedIndex, Forest}, }, + rand::{CryptoRng, Rand, RngCore}, }; use manta_util::future::{LocalBoxFuture, LocalBoxFutureResult}; use std::collections::{HashMap, HashSet}; use tokio::sync::RwLock; #[cfg(feature = "serde")] -use manta_util::serde::{Deserialize, Serialize}; +use { + manta_crypto::arkworks::serialize::{canonical_deserialize, canonical_serialize}, + manta_util::serde::{Deserialize, Serialize}, +}; #[cfg(feature = "http")] #[cfg_attr(doc_cfg, doc(cfg(feature = "http")))] @@ -110,6 +122,11 @@ impl AsRef for WrapPair { } /// Ledger +#[cfg_attr( + feature = "serde", + derive(Deserialize, Serialize), + serde(crate = "manta_util::serde", deny_unknown_fields) +)] #[derive(Debug)] pub struct Ledger { /// Nullifier @@ -128,6 +145,13 @@ pub struct Ledger { accounts: HashMap>, /// Verifying Contexts + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "canonical_serialize::", + deserialize_with = "canonical_deserialize::<'de, _, MultiVerifyingContext>" + ) + )] verifying_context: MultiVerifyingContext, /// UTXO Configuration Parameters @@ -218,6 +242,25 @@ impl Ledger { true } + /// Pushes the data from `posts` to the ledger without running the validation checks. + /// See the documentation of [`dont_validate`](UnsafeLedger::dont_validate) for more info. + #[inline] + pub fn unsafe_push(&mut self, account: AccountId, posts: Vec) -> bool { + for post in posts { + let (sources, sinks) = match TransferShape::from_post(&post) { + Some(TransferShape::ToPrivate) => (vec![account], vec![]), + Some(TransferShape::PrivateTransfer) => (vec![], vec![]), + Some(TransferShape::ToPublic) => (vec![], vec![account]), + _ => return false, + }; + let _ = self + .dont_validate(post, sources, sinks) + .post(&mut *self, &()); // it won't unwrap in general because `dont_validate` + // doesn't check the public participants. + } + true + } + /// Pulls the data from the ledger necessary to perform an [`initial_sync`]. /// /// [`initial_sync`]: manta_accounting::wallet::signer::Connection::initial_sync @@ -259,6 +302,16 @@ impl Ledger { pub fn utxos(&self) -> &HashSet { &self.utxos } + + /// Serializaes `self` into `writer`. + #[cfg(all(feature = "serde", feature = "std"))] + #[inline] + pub fn serialize_into(&self, writer: W) + where + W: std::io::Write, + { + bincode::serialize_into(writer, self).expect("Serialization error") + } } /// Sender Ledger Error @@ -645,9 +698,228 @@ impl TransferLedger for Ledger { } } +impl UnsafeSenderLedger for Ledger { + #[inline] + fn dont_check_utxo_accumulator_output( + &self, + output: UtxoAccumulatorOutput, + ) -> Self::ValidUtxoAccumulatorOutput { + Wrap(output) + } + + #[inline] + fn dont_check_nullifier(&self, nullifier: Nullifier) -> Self::ValidNullifier { + Wrap(nullifier) + } +} + +impl UnsafeReceiverLedger for Ledger { + #[inline] + fn dont_check_registration(&self, utxo: Utxo) -> Self::ValidUtxo { + Wrap(utxo) + } +} + +impl UnsafeLedger for Ledger { + #[inline] + fn dont_check_source_accounts( + &self, + asset_id: &AssetId, + sources: I, + ) -> Vec + where + I: Iterator, + { + let _ = asset_id; + sources + .map(|(account_id, withdraw)| WrapPair(account_id, withdraw)) + .collect() + } + + #[inline] + fn dont_check_sink_accounts( + &self, + asset_id: &AssetId, + sinks: I, + ) -> Vec + where + I: Iterator, + { + let _ = asset_id; + sinks + .map(|(account_id, withdraw)| WrapPair(account_id, withdraw)) + .collect() + } + + #[inline] + fn dont_check_proof( + &self, + posting_key: TransferPostingKeyRef, + ) -> (Self::ValidProof, Self::Event) { + let _ = posting_key; + (Wrap(()), ()) + } +} + /// Shared Ledger pub type SharedLedger = Arc>; +/// Fills `ledger` with randomly sampled `number_of_coins` of transactions. +pub async fn safe_fill_ledger( + number_of_coins: usize, + ledger: &SharedLedger, + proving_context: &MultiProvingContext, + parameters: &Parameters, + utxo_accumulator_model: &UtxoAccumulatorModel, + asset_id: AssetId, + rng: &mut R, +) where + R: RngCore + CryptoRng + ?Sized, +{ + let mut utxo_accumulator = UtxoAccumulator::new(utxo_accumulator_model.clone()); + for i in 0..number_of_coins { + if i % 5 == 0 { + println!("{i:?}"); + } + match rng.gen_range(0..3) { + 0 => { + let account = rng.gen(); + let asset_value = rng.gen(); + let to_private = to_private( + &proving_context.to_private, + parameters, + &mut utxo_accumulator, + asset_id, + asset_value, + rng, + ); + ledger + .write() + .await + .set_public_balance(account, asset_id, asset_value); + ledger.write().await.push(rng.gen(), vec![to_private]); + } + 1 => { + let account = rng.gen(); + let asset_values = [rng.gen::<_, u128>() / 2, rng.gen::<_, u128>() / 2]; + let (private_transfer_input, private_transfer) = private_transfer( + proving_context, + parameters, + &mut utxo_accumulator, + asset_id, + asset_values, + rng, + ); + ledger.write().await.set_public_balance( + account, + asset_id, + asset_values[0] + asset_values[1], + ); + ledger + .write() + .await + .push(account, private_transfer_input.into()); + ledger.write().await.push(account, vec![private_transfer]); + } + _ => { + let account = rng.gen(); + let asset_values = [rng.gen::<_, u128>() / 2, rng.gen::<_, u128>() / 2]; + let (to_public_input, to_public) = to_public( + proving_context, + parameters, + &mut utxo_accumulator, + asset_id, + asset_values, + account, + rng, + ); + ledger.write().await.set_public_balance( + account, + asset_id, + asset_values[0] + asset_values[1], + ); + ledger.write().await.push(account, to_public_input.into()); + ledger.write().await.push(account, vec![to_public]); + } + }; + } +} + +/// Fills `ledger` with randomly sampled `number_of_coins` of transactions. +/// +/// # Note +/// +/// This function does not perform any checks on the generated transactions, as +/// opposed to [`safe_fill_ledger`] which performs all necessary checks. See the documentation +/// of the [`unverified_transfers`] module for more information. +/// +/// [`unverified_transfers`]: manta_accounting::transfer::test::unverified_transfers +pub async fn unsafe_fill_ledger( + number_of_coins: usize, + ledger: &SharedLedger, + proving_context: &MultiProvingContext, + parameters: &Parameters, + utxo_accumulator_model: &UtxoAccumulatorModel, + asset_id: AssetId, + rng: &mut R, +) where + R: RngCore + CryptoRng + ?Sized, +{ + for _ in 0..number_of_coins { + match rng.gen_range(0..3) { + 0 => { + let to_private = unsafe_to_private( + &proving_context.to_private, + parameters, + utxo_accumulator_model, + asset_id, + rng.gen(), + rng, + ); + ledger + .write() + .await + .unsafe_push(rng.gen(), vec![to_private]); + } + 1 => { + let (private_transfer_input, private_transfer) = unsafe_private_transfer( + proving_context, + parameters, + utxo_accumulator_model, + asset_id, + [rng.gen::<_, u128>() / 2, rng.gen::<_, u128>() / 2], + rng, + ); + ledger + .write() + .await + .unsafe_push(rng.gen(), private_transfer_input.into()); + ledger + .write() + .await + .unsafe_push(rng.gen(), vec![private_transfer]); + } + _ => { + let account = rng.gen(); + let (to_public_input, to_public) = unsafe_to_public( + proving_context, + parameters, + utxo_accumulator_model, + asset_id, + [rng.gen::<_, u128>() / 2, rng.gen::<_, u128>() / 2], + account, + rng, + ); + ledger + .write() + .await + .unsafe_push(rng.gen(), to_public_input.into()); + ledger.write().await.unsafe_push(account, vec![to_public]); + } + }; + } +} + /// Ledger Connection #[derive(Clone, Debug)] pub struct LedgerConnection { diff --git a/manta-pay/src/test/payment.rs b/manta-pay/src/test/payment.rs index 61c1c3013..b0d2c20ec 100644 --- a/manta-pay/src/test/payment.rs +++ b/manta-pay/src/test/payment.rs @@ -406,3 +406,211 @@ pub mod to_public { .expect("") } } + +/// Unsafe [`ToPrivate`]. +/// +/// # Crypto Safety +/// +/// The [`TransferPost`]s generated here come with a default, invalid proof. Use +/// this module only for testing purposes and with trusted inputs. +pub mod unsafe_to_private { + use super::*; + + /// Generates a [`TransferPost`] for a [`ToPrivate`] transaction with custom `asset` as input. + #[inline] + pub fn unsafe_no_prove_full( + proving_context: &ProvingContext, + parameters: &Parameters, + utxo_accumulator_model: &UtxoAccumulatorModel, + asset_id: AssetId, + value: AssetValue, + rng: &mut R, + ) -> TransferPost + where + R: CryptoRng + RngCore + ?Sized, + { + ToPrivate::from_address( + parameters, + rng.gen(), + Asset::new(asset_id, value), + Default::default(), + rng, + ) + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + proving_context, + None, + Vec::new(), + rng, + ) + .into() + } +} + +/// Unsafe [`PrivateTransfer`]. +/// +/// # Crypto Safety +/// +/// The [`TransferPost`]s generated here come with a default, invalid proof and are not signed. +/// Use this module only for testing purposes and with trusted inputs. +pub mod unsafe_private_transfer { + use super::*; + + /// Generates a [`TransferPost`] for a [`PrivateTransfer`] transaction including pre-requisite + /// [`ToPrivate`] transactions. + #[inline] + pub fn unsafe_no_prove_full( + proving_context: &MultiProvingContext, + parameters: &Parameters, + utxo_accumulator_model: &UtxoAccumulatorModel, + asset_id: AssetId, + values: [AssetValue; 2], + rng: &mut R, + ) -> ([TransferPost; 2], TransferPost) + where + R: CryptoRng + RngCore + ?Sized, + { + let asset_0 = Asset::new(asset_id, values[0]); + let asset_1 = Asset::new(asset_id, values[1]); + let spending_key = rng.gen(); + let address = parameters.address_from_spending_key(&spending_key); + let mut authorization = Authorization::from_spending_key(parameters, &spending_key, rng); + + let (to_private_0, pre_sender_0) = ToPrivate::internal_pair( + parameters, + &mut authorization.context, + address, + asset_0, + Default::default(), + rng, + ); + let to_private_0 = to_private_0 + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + &proving_context.to_private, + None, + Vec::new(), + rng, + ) + .into(); + let sender_0 = pre_sender_0.assign_default_proof_unchecked(); + let receiver_0 = Receiver::sample(parameters, address, asset_1, Default::default(), rng); + let (to_private_1, pre_sender_1) = ToPrivate::internal_pair( + parameters, + &mut authorization.context, + address, + asset_1, + Default::default(), + rng, + ); + let to_private_1 = to_private_1 + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + &proving_context.to_private, + None, + Vec::new(), + rng, + ) + .into(); + let sender_1 = pre_sender_1.assign_default_proof_unchecked(); + let receiver_1 = Receiver::sample(parameters, address, asset_0, Default::default(), rng); + let private_transfer = PrivateTransfer::build( + authorization, + [sender_0, sender_1], + [receiver_1, receiver_0], + ) + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + &proving_context.private_transfer, + Some(&spending_key), + Vec::new(), + rng, + ) + .into(); + + ([to_private_0, to_private_1], private_transfer) + } +} + +/// Unsafe [`ToPublic`]. +/// +/// # Crypto Safety +/// +/// The [`TransferPost`]s generated here come with a default, invalid proof and are not signed. +/// Use this module only for testing purposes and with trusted inputs. +pub mod unsafe_to_public { + use super::*; + + /// Generates a [`TransferPost`] for a [`ToPublic`] transaction including pre-requisite [`ToPrivate`] + /// transactions. + #[inline] + pub fn unsafe_no_prove_full( + proving_context: &MultiProvingContext, + parameters: &Parameters, + utxo_accumulator_model: &UtxoAccumulatorModel, + asset_id: AssetId, + values: [AssetValue; 2], + public_account: AccountId, + rng: &mut R, + ) -> ([TransferPost; 2], TransferPost) + where + R: CryptoRng + RngCore + ?Sized, + { + let asset_0 = Asset::new(asset_id, values[0]); + let asset_1 = Asset::new(asset_id, values[1]); + let spending_key = rng.gen(); + let address = parameters.address_from_spending_key(&spending_key); + let mut authorization = Authorization::from_spending_key(parameters, &spending_key, rng); + + let (to_private_0, pre_sender_0) = ToPrivate::internal_pair( + parameters, + &mut authorization.context, + address, + asset_0, + Default::default(), + rng, + ); + let to_private_0 = to_private_0 + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + &proving_context.to_private, + None, + Vec::new(), + rng, + ) + .into(); + let sender_0 = pre_sender_0.assign_default_proof_unchecked(); + + let (to_private_1, pre_sender_1) = ToPrivate::internal_pair( + parameters, + &mut authorization.context, + address, + asset_1, + Default::default(), + rng, + ); + let to_private_1 = to_private_1 + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + &proving_context.to_private, + None, + Vec::new(), + rng, + ) + .into(); + let sender_1 = pre_sender_1.assign_default_proof_unchecked(); + let receiver_1 = Receiver::sample(parameters, address, asset_0, Default::default(), rng); + + let to_public = ToPublic::build(authorization, [sender_0, sender_1], [receiver_1], asset_1) + .into_unsafe_post( + FullParametersRef::new(parameters, utxo_accumulator_model), + &proving_context.to_public, + Some(&spending_key), + Vec::from([public_account]), + rng, + ) + .into(); + + ([to_private_0, to_private_1], to_public) + } +} diff --git a/manta-pay/src/test/signer.rs b/manta-pay/src/test/signer.rs index 5fa5df486..7e6d7e1dc 100644 --- a/manta-pay/src/test/signer.rs +++ b/manta-pay/src/test/signer.rs @@ -24,15 +24,27 @@ use crate::{ base::identity_verification, functions::{address_from_mnemonic, authorization_context_from_mnemonic}, }, - simulation::sample_signer, + simulation::{ + ledger::{Ledger, LedgerConnection, SharedLedger}, + sample_signer, + }, +}; +use alloc::sync::Arc; +use manta_accounting::{ + transfer::{canonical::Transaction, IdentifiedAsset, Identifier}, + wallet::{test::PublicBalanceOracle, Wallet}, }; -use manta_accounting::transfer::{canonical::Transaction, IdentifiedAsset, Identifier}; use manta_crypto::{ algebra::HasGenerator, arkworks::constraint::fp::Fp, - rand::{fuzz::Fuzz, OsRng, Rand}, + rand::{fuzz::Fuzz, CryptoRng, OsRng, Rand, RngCore}, }; use manta_util::vec::VecExt; +use std::{env, fs::OpenOptions, io::Error}; +use tokio::sync::RwLock; + +/// Test Wallet type +type TestWallet = Wallet; /// Checks the generation and verification of [`IdentityProof`](manta_accounting::transfer::IdentityProof)s. #[test] @@ -172,3 +184,125 @@ pub fn derive_address_works() { "Both receiving keys should be the same" ); } + +/// Loads the precomputed ledger in the `data` folder. +pub fn load_ledger() -> Result { + let data_dir = env::current_dir() + .expect("Failed to get current directory") + .join("src/test/data"); + let target_file = OpenOptions::new() + .create_new(false) + .read(true) + .open(data_dir.join("precomputed_ledger"))?; + let ledger: Ledger = bincode::deserialize_from(&target_file).expect("Deserialization error"); + Ok(Arc::new(RwLock::new(ledger))) +} + +/// Creates new wallet from `account_id`, `initial_balance`, `asset_id` and `ledger`. +async fn create_new_wallet( + account_id: [u8; 32], + initial_balance: u128, + asset_id: u128, + ledger: SharedLedger, + rng: &mut R, +) -> TestWallet +where + R: CryptoRng + RngCore + ?Sized, +{ + let directory = tempfile::tempdir().expect("Unable to generate temporary test directory."); + let (proving_context, _, parameters, utxo_accumulator_model) = + load_parameters(directory.path()).expect("Failed to load parameters"); + let signer = sample_signer(&proving_context, ¶meters, &utxo_accumulator_model, rng); + let asset_id = asset_id.into(); + ledger + .write() + .await + .set_public_balance(account_id, asset_id, initial_balance); + let ledger_connection = LedgerConnection::new(account_id, ledger); + TestWallet::new(ledger_connection, signer) +} + +/// This tests that wallets preserve the invariants. After loading the [`SharedLedger`], +/// it executes `NUMBER_OF_RUNS` times the following steps: +/// 1) Creates a new wallet, resets it and syncs it. +/// 2) Privatizes some tokens and syncs. +/// 3) Sends some private tokens to another zkAddress and syncs. +/// 4) Reclaims some tokens and syncs. +/// 5) Restarts the wallet. +/// 6) Checks that the public and private balances are correct. +#[ignore] // We don't run this test on the CI because it takes a long time to run. +#[tokio::test] +async fn find_the_bug() { + let mut rng = OsRng; + let asset_id = 8; + println!("Loading ledger"); + let ledger = load_ledger().expect("Error loading ledger"); + const NUMBER_OF_RUNS: usize = 100; + for run in 0..NUMBER_OF_RUNS { + println!("Run number {:?}", run + 1); + // 1) create new wallet, reset it and sync. (zkBalance = 0) + let account_id = rng.gen(); // fixed account id + let initial_balance = rng.gen_range(Default::default()..u32::MAX as u128); + println!("Initial public balance: {initial_balance:?}"); + let mut wallet = create_new_wallet( + account_id, + initial_balance, + asset_id, + ledger.clone(), + &mut rng, + ) + .await; + wallet.reset_state(); + wallet.load_initial_state().await.expect("Sync error"); + wallet.sync().await.expect("Sync error"); + // 2) privatize `to_mint` tokens and sync (zkBalance = to_mint) + let to_mint = rng.gen_range(Default::default()..initial_balance); + println!("To Private: {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) send `to_send` tokens to another zkAddress and sync (zkBalance = to_mint - to_send) + let to_send = rng.gen_range(Default::default()..to_mint); + println!("Private Transfer: {to_send:?}"); + let private_transfer = + Transaction::::PrivateTransfer(Asset::new(asset_id.into(), to_send), rng.gen()); + wallet + .post(private_transfer, Default::default()) + .await + .expect("Error posting PrivateTransfer"); + wallet.sync().await.expect("Sync error"); + // 4) reclaim `reclaim` tokens and sync (zkBalance = to_mint - to_send - reclaim) + let reclaim = rng.gen_range(Default::default()..to_mint - to_send); + println!("To Public: {reclaim:?}"); + let to_public = + Transaction::::ToPublic(Asset::new(asset_id.into(), reclaim), account_id); + wallet + .post(to_public, Default::default()) + .await + .expect("Error posting ToPublic"); + wallet.sync().await.expect("Sync error"); + // 5: Restart wallet + wallet.reset_state(); + wallet.load_initial_state().await.expect("Sync error"); + wallet.sync().await.expect("Sync error"); + // 6: check balances + let public_balance = match wallet.ledger().public_balances().await { + Some(asset_list) => asset_list.value(&asset_id.into()), + None => 0, + }; + let private_balance = wallet.balance(&asset_id.into()); + let correct_public_balance = initial_balance - to_mint + reclaim; + let correct_private_balance = to_mint - to_send - reclaim; + assert_eq!( + correct_private_balance, private_balance, + "Private balance is not correct" + ); + assert_eq!( + correct_public_balance, public_balance, + "Public balance is not correct" + ); + } +}