11//! MASP verification wrappers.
22
3- use std:: collections:: hash_map:: Entry ;
4- use std:: collections:: { BTreeMap , HashMap , HashSet } ;
3+ use std:: collections:: { btree_map, BTreeMap , BTreeSet , HashMap , HashSet } ;
54use std:: env;
65use std:: fmt:: Debug ;
76#[ cfg( feature = "masp-tx-gen" ) ]
@@ -79,6 +78,17 @@ use crate::types::transaction::{EllipticCurve, PairingEngine, WrapperTx};
7978/// the default OS specific path is used.
8079pub const ENV_VAR_MASP_PARAMS_DIR : & str = "NAMADA_MASP_PARAMS_DIR" ;
8180
81+ /// Env var to either "save" proofs into files or to "load" them from
82+ /// files.
83+ pub const ENV_VAR_MASP_TEST_PROOFS : & str = "NAMADA_MASP_TEST_PROOFS" ;
84+
85+ /// Randomness seed for MASP integration tests to build proofs with
86+ /// deterministic rng.
87+ pub const ENV_VAR_MASP_TEST_SEED : & str = "NAMADA_MASP_TEST_SEED" ;
88+
89+ /// A directory to save serialized proofs for tests.
90+ pub const MASP_TEST_PROOFS_DIR : & str = "test_fixtures/masp_proofs" ;
91+
8292/// The network to use for MASP
8393#[ cfg( feature = "mainnet" ) ]
8494const NETWORK : MainNetwork = MainNetwork ;
@@ -93,6 +103,26 @@ pub const OUTPUT_NAME: &str = "masp-output.params";
93103/// Convert circuit name
94104pub const CONVERT_NAME : & str = "masp-convert.params" ;
95105
106+ /// Shielded transfer
107+ #[ derive( Clone , Debug , BorshSerialize , BorshDeserialize ) ]
108+ pub struct ShieldedTransfer {
109+ /// Shielded transfer builder
110+ pub builder : Builder < ( ) , ( ) , ExtendedFullViewingKey , ( ) > ,
111+ /// MASP transaction
112+ pub masp_tx : Transaction ,
113+ /// Metadata
114+ pub metadata : SaplingMetadata ,
115+ /// Epoch in which the transaction was created
116+ pub epoch : Epoch ,
117+ }
118+
119+ #[ derive( Clone , Copy , Debug ) ]
120+ enum LoadOrSaveProofs {
121+ Load ,
122+ Save ,
123+ Neither ,
124+ }
125+
96126fn load_pvks ( ) -> (
97127 PreparedVerifyingKey < Bls12 > ,
98128 PreparedVerifyingKey < Bls12 > ,
@@ -511,7 +541,7 @@ impl From<MaspAmount> for Amount {
511541
512542/// Represents the amount used of different conversions
513543pub type Conversions =
514- HashMap < AssetType , ( AllowedConversion , MerklePath < Node > , i128 ) > ;
544+ BTreeMap < AssetType , ( AllowedConversion , MerklePath < Node > , i128 ) > ;
515545
516546/// Represents the changes that were made to a list of transparent accounts
517547pub type TransferDelta = HashMap < Address , MaspChange > ;
@@ -531,7 +561,7 @@ pub struct ShieldedContext<U: ShieldedUtils> {
531561 /// The commitment tree produced by scanning all transactions up to tx_pos
532562 pub tree : CommitmentTree < Node > ,
533563 /// Maps viewing keys to applicable note positions
534- pub pos_map : HashMap < ViewingKey , HashSet < usize > > ,
564+ pub pos_map : HashMap < ViewingKey , BTreeSet < usize > > ,
535565 /// Maps a nullifier to the note position to which it applies
536566 pub nf_map : HashMap < Nullifier , usize > ,
537567 /// Maps note positions to their corresponding notes
@@ -657,7 +687,7 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
657687 ..Default :: default ( )
658688 } ;
659689 for vk in unknown_keys {
660- tx_ctx. pos_map . entry ( vk) . or_insert_with ( HashSet :: new) ;
690+ tx_ctx. pos_map . entry ( vk) . or_insert_with ( BTreeSet :: new) ;
661691 }
662692 // Update this unknown shielded context until it is level with self
663693 while tx_ctx. last_txidx != self . last_txidx {
@@ -931,7 +961,9 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
931961 asset_type : AssetType ,
932962 conversions : & ' a mut Conversions ,
933963 ) {
934- if let Entry :: Vacant ( conv_entry) = conversions. entry ( asset_type) {
964+ if let btree_map:: Entry :: Vacant ( conv_entry) =
965+ conversions. entry ( asset_type)
966+ {
935967 // Query for the ID of the last accepted transaction
936968 if let Some ( ( addr, denom, ep, conv, path) ) =
937969 query_conversion ( client, asset_type) . await
@@ -962,7 +994,7 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
962994 client,
963995 balance,
964996 target_epoch,
965- HashMap :: new ( ) ,
997+ BTreeMap :: new ( ) ,
966998 )
967999 . await
9681000 . 0 ;
@@ -1142,7 +1174,7 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
11421174 Conversions ,
11431175 ) {
11441176 // Establish connection with which to do exchange rate queries
1145- let mut conversions = HashMap :: new ( ) ;
1177+ let mut conversions = BTreeMap :: new ( ) ;
11461178 let mut val_acc = Amount :: zero ( ) ;
11471179 let mut notes = Vec :: new ( ) ;
11481180 // Retrieve the notes that can be spent by this key
@@ -1286,7 +1318,7 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
12861318 println ! ( "Decoded pinned balance: {:?}" , amount) ;
12871319 // Finally, exchange the balance to the transaction's epoch
12881320 let computed_amount = self
1289- . compute_exchanged_amount ( client, amount, ep, HashMap :: new ( ) )
1321+ . compute_exchanged_amount ( client, amount, ep, BTreeMap :: new ( ) )
12901322 . await
12911323 . 0 ;
12921324 println ! ( "Exchanged amount: {:?}" , computed_amount) ;
@@ -1357,16 +1389,17 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
13571389 args : & args:: TxTransfer ,
13581390 shielded_gas : bool ,
13591391 ) -> Result <
1360- Option < (
1361- Builder < ( ) , ( ) , ExtendedFullViewingKey , ( ) > ,
1362- Transaction ,
1363- SaplingMetadata ,
1364- Epoch ,
1365- ) > ,
1392+ Option < ShieldedTransfer > ,
13661393 builder:: Error < std:: convert:: Infallible > ,
13671394 > {
13681395 // No shielded components are needed when neither source nor destination
13691396 // are shielded
1397+
1398+ use std:: str:: FromStr ;
1399+
1400+ use rand:: rngs:: StdRng ;
1401+ use rand_core:: SeedableRng ;
1402+
13701403 let spending_key = args. source . spending_key ( ) ;
13711404 let payment_address = args. target . payment_address ( ) ;
13721405 // No shielded components are needed when neither source nor
@@ -1388,8 +1421,27 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
13881421 // possesion
13891422 let memo = MemoBytes :: empty ( ) ;
13901423
1424+ // Try to get a seed from env var, if any.
1425+ let rng = if let Ok ( seed) =
1426+ env:: var ( ENV_VAR_MASP_TEST_SEED ) . map ( |seed| {
1427+ let exp_str =
1428+ format ! ( "Env var {ENV_VAR_MASP_TEST_SEED} must be a u64." ) ;
1429+ let parsed_seed: u64 =
1430+ FromStr :: from_str ( & seed) . expect ( & exp_str) ;
1431+ parsed_seed
1432+ } ) {
1433+ tracing:: warn!(
1434+ "UNSAFE: Using a seed from {ENV_VAR_MASP_TEST_SEED} env var \
1435+ to build proofs."
1436+ ) ;
1437+ StdRng :: seed_from_u64 ( seed)
1438+ } else {
1439+ StdRng :: from_rng ( OsRng ) . unwrap ( )
1440+ } ;
1441+
13911442 // Now we build up the transaction within this object
1392- let mut builder = Builder :: < TestNetwork , OsRng > :: new ( NETWORK , 1 . into ( ) ) ;
1443+ let mut builder =
1444+ Builder :: < TestNetwork , _ > :: new_with_rng ( NETWORK , 1 . into ( ) , rng) ;
13931445
13941446 // break up a transfer into a number of transfers with suitable
13951447 // denominations
@@ -1548,16 +1600,81 @@ impl<U: ShieldedUtils> ShieldedContext<U> {
15481600 }
15491601 }
15501602
1551- // Build and return the constructed transaction
1552- builder
1553- . clone ( )
1554- . build (
1603+ // To speed up integration tests, we can save and load proofs
1604+ let load_or_save = if let Ok ( masp_proofs) =
1605+ env:: var ( ENV_VAR_MASP_TEST_PROOFS )
1606+ {
1607+ let parsed = match masp_proofs. to_ascii_lowercase ( ) . as_str ( ) {
1608+ "load" => LoadOrSaveProofs :: Load ,
1609+ "save" => LoadOrSaveProofs :: Save ,
1610+ env_var => panic ! (
1611+ "Unexpected value for {ENV_VAR_MASP_TEST_PROOFS} env var. \
1612+ Expecting \" save\" or \" load\" , but got \" {env_var}\" ."
1613+ ) ,
1614+ } ;
1615+ if env:: var ( ENV_VAR_MASP_TEST_SEED ) . is_err ( ) {
1616+ panic ! (
1617+ "Ensure to set a seed with {ENV_VAR_MASP_TEST_SEED} env \
1618+ var when using {ENV_VAR_MASP_TEST_PROOFS} for \
1619+ deterministic proofs."
1620+ ) ;
1621+ }
1622+ parsed
1623+ } else {
1624+ LoadOrSaveProofs :: Neither
1625+ } ;
1626+
1627+ let builder_clone = builder. clone ( ) . map_builder ( WalletMap ) ;
1628+ let builder_bytes = BorshSerialize :: try_to_vec ( & builder_clone) . unwrap ( ) ;
1629+ let builder_hash =
1630+ namada_core:: types:: hash:: Hash :: sha256 ( & builder_bytes) ;
1631+ let saved_filepath = env:: current_dir ( )
1632+ . unwrap ( )
1633+ // One up from "tests" dir to the root dir
1634+ . parent ( )
1635+ . unwrap ( )
1636+ . join ( MASP_TEST_PROOFS_DIR )
1637+ . join ( format ! ( "{builder_hash}.bin" ) ) ;
1638+
1639+ if let LoadOrSaveProofs :: Load = load_or_save {
1640+ let recommendation = format ! (
1641+ "Re-run the tests with {ENV_VAR_MASP_TEST_PROOFS}=save to \
1642+ re-generate proofs."
1643+ ) ;
1644+ let exp_str = format ! (
1645+ "Read saved MASP proofs from {}. {recommendation}" ,
1646+ saved_filepath. to_string_lossy( )
1647+ ) ;
1648+ let loaded_bytes =
1649+ tokio:: fs:: read ( & saved_filepath) . await . expect ( & exp_str) ;
1650+ let exp_str = format ! (
1651+ "Valid `ShieldedTransfer` bytes in {}. {recommendation}" ,
1652+ saved_filepath. to_string_lossy( )
1653+ ) ;
1654+ let loaded: ShieldedTransfer =
1655+ BorshDeserialize :: try_from_slice ( & loaded_bytes)
1656+ . expect ( & exp_str) ;
1657+ Ok ( Some ( loaded) )
1658+ } else {
1659+ // Build and return the constructed transaction
1660+ let ( masp_tx, metadata) = builder. build (
15551661 & self . utils . local_tx_prover ( ) ,
15561662 & FeeRule :: non_standard ( tx_fee) ,
1557- )
1558- . map ( |( tx, metadata) | {
1559- Some ( ( builder. map_builder ( WalletMap ) , tx, metadata, epoch) )
1560- } )
1663+ ) ?;
1664+ let built = ShieldedTransfer {
1665+ builder : builder_clone,
1666+ masp_tx,
1667+ metadata,
1668+ epoch,
1669+ } ;
1670+ if let LoadOrSaveProofs :: Save = load_or_save {
1671+ let built_bytes = BorshSerialize :: try_to_vec ( & built) . unwrap ( ) ;
1672+ tokio:: fs:: write ( & saved_filepath, built_bytes)
1673+ . await
1674+ . unwrap ( ) ;
1675+ }
1676+ Ok ( Some ( built) )
1677+ }
15611678 }
15621679
15631680 /// Obtain the known effects of all accepted shielded and transparent
0 commit comments