Skip to content

Commit 01c63da

Browse files
committed
Merge branch 'tomas/pregen-masp-proofs' (#1772)
* origin/tomas/pregen-masp-proofs: changelog: add #1768 make: add recipes for integration tests with saved MASP proofs test: add masp_proofs test fixtures shared/masp: allow to save and load proofs for tests
2 parents 8e45127 + 83f5c2d commit 01c63da

18 files changed

+190
-35
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Added pre-built MASP proofs for integration tests.
2+
([\#1768](https://github.com/anoma/namada/pull/1768))

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ members = [
2020
exclude = [
2121
"wasm",
2222
"wasm_for_tests",
23+
"test_fixtures",
2324
]
2425

2526
[workspace.package]

Makefile

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package = namada
44
NAMADA_E2E_USE_PREBUILT_BINARIES ?= true
55
NAMADA_E2E_DEBUG ?= true
66
RUST_BACKTRACE ?= 1
7+
NAMADA_MASP_TEST_SEED ?= 0
78

89
cargo := $(env) cargo
910
rustup := $(env) rustup
@@ -116,12 +117,14 @@ audit:
116117

117118
test: test-unit test-e2e test-wasm
118119

119-
# Unit tests with coverage report
120120
test-coverage:
121+
# Run integration tests with pre-built MASP proofs
122+
NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \
123+
NAMADA_MASP_TEST_PROOFS=load \
121124
$(cargo) +$(nightly) llvm-cov --output-dir target \
122125
--features namada/testing \
123126
--html \
124-
-- --skip e2e --skip integration -Z unstable-options --report-time
127+
-- --skip e2e -Z unstable-options --report-time
125128

126129
# NOTE: `TEST_FILTER` is prepended with `e2e::`. Since filters in `cargo test`
127130
# work with a substring search, TEST_FILTER only works if it contains a string
@@ -137,7 +140,23 @@ test-e2e:
137140
--test-threads=1 \
138141
-Z unstable-options --report-time
139142

143+
# Run integration tests with pre-built MASP proofs
140144
test-integration:
145+
NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \
146+
NAMADA_MASP_TEST_PROOFS=load \
147+
make test-integration-slow
148+
149+
# Clear pre-built proofs, run integration tests and save the new proofs
150+
test-integration-save-proofs:
151+
# Clear old proofs first
152+
rm --force test_fixtures/masp_proofs/*.bin || true
153+
NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \
154+
NAMADA_MASP_TEST_PROOFS=save \
155+
TEST_FILTER=masp \
156+
make test-integration-slow
157+
158+
# Run integration tests without specifiying any pre-built MASP proofs option
159+
test-integration-slow:
141160
RUST_BACKTRACE=$(RUST_BACKTRACE) \
142161
$(cargo) +$(nightly) test integration::$(TEST_FILTER) \
143162
-Z unstable-options \

shared/src/ledger/masp.rs

Lines changed: 141 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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};
54
use std::env;
65
use 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.
8079
pub 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")]
8494
const NETWORK: MainNetwork = MainNetwork;
@@ -93,6 +103,26 @@ pub const OUTPUT_NAME: &str = "masp-output.params";
93103
/// Convert circuit name
94104
pub 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+
96126
fn 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
513543
pub 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
517547
pub 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

shared/src/ledger/tx.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use crate::ibc::tx_msg::Msg;
4242
use crate::ibc::Height as IbcHeight;
4343
use crate::ibc_proto::cosmos::base::v1beta1::Coin;
4444
use crate::ledger::args::{self, InputAmount};
45-
use crate::ledger::masp::{ShieldedContext, ShieldedUtils};
45+
use crate::ledger::masp::{ShieldedContext, ShieldedTransfer, ShieldedUtils};
4646
use crate::ledger::rpc::{
4747
self, format_denominated_amount, validate_amount, TxBroadcastData,
4848
TxResponse,
@@ -1132,7 +1132,8 @@ pub async fn build_default_proposal<
11321132
init_proposal_data.content = extra_section_hash;
11331133

11341134
if let Some(init_proposal_code) = proposal.data {
1135-
let (_, extra_section_hash) = tx_builder.add_extra_section(init_proposal_code);
1135+
let (_, extra_section_hash) =
1136+
tx_builder.add_extra_section(init_proposal_code);
11361137
init_proposal_data.r#type =
11371138
ProposalType::Default(Some(extra_section_hash));
11381139
};
@@ -1607,30 +1608,34 @@ pub async fn build_transfer<
16071608
let mut tx = Tx::new(chain_id, args.tx.expiration);
16081609

16091610
// Add the MASP Transaction and its Builder to facilitate validation
1610-
let (masp_hash, shielded_tx_epoch) = if let Some(shielded_parts) =
1611-
shielded_parts
1611+
let (masp_hash, shielded_tx_epoch) = if let Some(ShieldedTransfer {
1612+
builder,
1613+
masp_tx,
1614+
metadata,
1615+
epoch,
1616+
}) = shielded_parts
16121617
{
16131618
// Add a MASP Transaction section to the Tx and get the tx hash
1614-
let masp_tx_hash = tx.add_masp_tx_section(shielded_parts.1).1;
1619+
let masp_tx_hash = tx.add_masp_tx_section(masp_tx).1;
16151620

16161621
// Get the decoded asset types used in the transaction to give
16171622
// offline wallet users more information
1618-
let asset_types = used_asset_types(shielded, client, &shielded_parts.0)
1623+
let asset_types = used_asset_types(shielded, client, &builder)
16191624
.await
16201625
.unwrap_or_default();
16211626

16221627
tx.add_masp_builder(MaspBuilder {
16231628
asset_types,
16241629
// Store how the Info objects map to Descriptors/Outputs
1625-
metadata: shielded_parts.2,
1630+
metadata,
16261631
// Store the data that was used to construct the Transaction
1627-
builder: shielded_parts.0,
1632+
builder,
16281633
// Link the Builder to the Transaction by hash code
16291634
target: masp_tx_hash,
16301635
});
16311636

16321637
// The MASP Transaction section hash will be used in Transfer
1633-
(Some(masp_tx_hash), Some(shielded_parts.3))
1638+
(Some(masp_tx_hash), Some(epoch))
16341639
} else {
16351640
(None, None)
16361641
};
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)