From d056b1383594e94037a088f1abe63eebcfaef0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 17 Jun 2024 15:24:45 +0100 Subject: [PATCH 1/3] move masp validation from SDK into shielded_token crate --- Cargo.lock | 5 +- Cargo.toml | 1 - crates/namada/Cargo.toml | 1 + crates/namada/src/ledger/native_vp/masp.rs | 2 +- crates/node/src/lib.rs | 2 +- crates/sdk/Cargo.toml | 6 +- crates/sdk/src/masp.rs | 366 ++------------------- crates/shielded_token/Cargo.toml | 12 +- crates/shielded_token/src/lib.rs | 1 + crates/shielded_token/src/validation.rs | 351 ++++++++++++++++++++ crates/token/Cargo.toml | 3 +- wasm/Cargo.lock | 6 +- wasm_for_tests/Cargo.lock | 6 +- 13 files changed, 404 insertions(+), 358 deletions(-) create mode 100644 crates/shielded_token/src/validation.rs diff --git a/Cargo.lock b/Cargo.lock index c5f83204df..9c12594851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5232,7 +5232,6 @@ dependencies = [ "async-trait", "base58", "bimap", - "bls12_381", "borsh 1.2.1", "borsh-ext", "circular-queue", @@ -5301,13 +5300,17 @@ name = "namada_shielded_token" version = "0.39.0" dependencies = [ "borsh 1.2.1", + "lazy_static", "masp_primitives", + "masp_proofs", "namada_controller", "namada_core", + "namada_gas", "namada_parameters", "namada_storage", "namada_trans_token", "proptest", + "rand_core 0.6.4", "rayon", "serde", "smooth-operator", diff --git a/Cargo.toml b/Cargo.toml index 6821a58e31..d9ce2699f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,6 @@ bimap = {version = "0.6.2", features = ["serde"]} bit-set = "0.5.2" bitflags = { version = "2.5.0", features = ["serde"] } blake2b-rs = "0.2.0" -bls12_381 = "0.8" byte-unit = "4.0.13" byteorder = "1.4.2" borsh = {version = "1.2.0", features = ["unstable__schema", "derive"]} diff --git a/crates/namada/Cargo.toml b/crates/namada/Cargo.toml index 502966354f..a2f4ba22f1 100644 --- a/crates/namada/Cargo.toml +++ b/crates/namada/Cargo.toml @@ -50,6 +50,7 @@ testing = [ "namada_proof_of_stake/testing", "namada_sdk/testing", "namada_state/testing", + "namada_token/testing", "async-client", "proptest", "tempfile", diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index 51e86f018b..00302bfecf 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -21,9 +21,9 @@ use namada_core::masp::{encode_asset_type, MaspEpoch}; use namada_core::storage::Key; use namada_gas::GasMetering; use namada_governance::storage::is_proposal_accepted; -use namada_sdk::masp::verify_shielded_tx; use namada_state::{ConversionState, OptionExt, ResultExt, StateRead}; use namada_token::read_denom; +use namada_token::validation::verify_shielded_tx; use namada_tx::BatchedTxRef; use namada_vp_env::VpEnv; use ripemd::Digest as RipemdDigest; diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index f210bb8a71..b9eb54dc4f 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -371,7 +371,7 @@ async fn run_aux( }; tracing::info!("Loading MASP verifying keys."); - let _ = namada_sdk::masp::preload_verifying_keys(); + let _ = namada::token::validation::preload_verifying_keys(); tracing::info!("Done loading MASP verifying keys."); // Start ABCI server and broadcaster (the latter only if we are a validator diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 6a3244a914..64faf3d0bd 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -51,11 +51,9 @@ testing = [ "async-client", "proptest", "jubjub", - "bls12_381", ] -# Download MASP params if they're not present -download-params = ["masp_proofs/download-params"] +download-params = ["namada_token/download-params"] migrations = [ "namada_migrations", "namada_account/migrations", @@ -92,7 +90,6 @@ namada_vote_ext = { path = "../vote_ext" } async-trait = { workspace = true, optional = true } bimap.workspace = true -bls12_381 = { workspace = true, optional = true } borsh.workspace = true borsh-ext.workspace = true circular-queue.workspace = true @@ -166,7 +163,6 @@ namada_vote_ext = {path = "../vote_ext"} assert_matches.workspace = true base58.workspace = true -bls12_381.workspace = true jubjub.workspace = true masp_primitives = { workspace = true, features = ["test-dependencies"] } proptest.workspace = true diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 516a7ad611..1b91a9d90c 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -4,12 +4,10 @@ use std::cmp::Ordering; use std::collections::{btree_map, BTreeMap, BTreeSet}; use std::env; use std::fmt::Debug; -use std::ops::Deref; use std::path::PathBuf; use borsh::{BorshDeserialize, BorshSerialize}; use borsh_ext::BorshSerializeExt; -use lazy_static::lazy_static; use masp_primitives::asset_type::AssetType; #[cfg(feature = "mainnet")] use masp_primitives::consensus::MainNetwork as Network; @@ -30,25 +28,15 @@ use masp_primitives::transaction::builder::{self, *}; use masp_primitives::transaction::components::sapling::builder::{ RngBuildParams, SaplingMetadata, }; -use masp_primitives::transaction::components::sapling::{ - Authorized as SaplingAuthorized, Bundle as SaplingBundle, -}; -use masp_primitives::transaction::components::transparent::builder::TransparentBuilder; use masp_primitives::transaction::components::{ I128Sum, OutputDescription, TxOut, U64Sum, ValueSum, }; use masp_primitives::transaction::fees::fixed::FeeRule; -use masp_primitives::transaction::sighash::{signature_hash, SignableInput}; -use masp_primitives::transaction::txid::TxIdDigester; use masp_primitives::transaction::{ - Authorization, Authorized, Transaction, TransactionData, - TransparentAddress, Unauthorized, + Authorization, Authorized, Transaction, TransparentAddress, }; use masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; -use masp_proofs::bellman::groth16::VerifyingKey; -use masp_proofs::bls12_381::Bls12; use masp_proofs::prover::LocalTxProver; -use masp_proofs::sapling::BatchValidator; use namada_core::address::Address; use namada_core::collections::{HashMap, HashSet}; use namada_core::dec::Dec; @@ -67,14 +55,16 @@ use namada_events::extend::{ use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; -use namada_state::StorageError; +pub use namada_token::validation::{ + partial_deauthorize, preload_verifying_keys, PVKs, CONVERT_NAME, + ENV_VAR_MASP_PARAMS_DIR, OUTPUT_NAME, SPEND_NAME, +}; use namada_token::{self as token, Denomination, MaspDigitPos}; use namada_tx::{IndexedTx, Tx}; use rand::rngs::StdRng; use rand_core::{CryptoRng, OsRng, RngCore, SeedableRng}; use ripemd::Digest as RipemdDigest; use sha2::Digest; -use smooth_operator::checked; use thiserror::Error; use crate::error::{Error, QueryError}; @@ -83,10 +73,6 @@ use crate::queries::Client; use crate::rpc::{query_block, query_conversion, query_denom}; use crate::{display_line, edisplay_line, rpc, MaybeSend, MaybeSync, Namada}; -/// Env var to point to a dir with MASP parameters. When not specified, -/// the default OS specific path is used. -pub const ENV_VAR_MASP_PARAMS_DIR: &str = "NAMADA_MASP_PARAMS_DIR"; - /// Randomness seed for MASP integration tests to build proofs with /// deterministic rng. pub const ENV_VAR_MASP_TEST_SEED: &str = "NAMADA_MASP_TEST_SEED"; @@ -94,14 +80,6 @@ pub const ENV_VAR_MASP_TEST_SEED: &str = "NAMADA_MASP_TEST_SEED"; /// The network to use for MASP const NETWORK: Network = Network; -// TODO these could be exported from masp_proof crate -/// Spend circuit name -pub const SPEND_NAME: &str = "masp-spend.params"; -/// Output circuit name -pub const OUTPUT_NAME: &str = "masp-output.params"; -/// Convert circuit name -pub const CONVERT_NAME: &str = "masp-convert.params"; - /// Type alias for convenience and profit pub type IndexedNoteData = BTreeMap>; @@ -144,261 +122,6 @@ pub enum TransferErr { General(#[from] Error), } -/// MASP verifying keys -pub struct PVKs { - /// spend verifying key - pub spend_vk: VerifyingKey, - /// convert verifying key - pub convert_vk: VerifyingKey, - /// output verifying key - pub output_vk: VerifyingKey, -} - -lazy_static! { - /// MASP verifying keys load from parameters - static ref VERIFIYING_KEYS: PVKs = - { - let params_dir = get_params_dir(); - let [spend_path, convert_path, output_path] = - [SPEND_NAME, CONVERT_NAME, OUTPUT_NAME].map(|p| params_dir.join(p)); - - #[cfg(feature = "download-params")] - if !spend_path.exists() || !convert_path.exists() || !output_path.exists() { - let paths = masp_proofs::download_masp_parameters(None).expect( - "MASP parameters were not present, expected the download to \ - succeed", - ); - if paths.spend != spend_path - || paths.convert != convert_path - || paths.output != output_path - { - panic!( - "unrecoverable: downloaded missing masp params, but to an \ - unfamiliar path" - ) - } - } - // size and blake2b checked here - let params = masp_proofs::load_parameters( - spend_path.as_path(), - output_path.as_path(), - convert_path.as_path(), - ); - PVKs { - spend_vk: params.spend_params.vk, - convert_vk: params.convert_params.vk, - output_vk: params.output_params.vk - } - }; -} - -/// Make sure the MASP params are present and load verifying keys into memory -pub fn preload_verifying_keys() -> &'static PVKs { - &VERIFIYING_KEYS -} - -fn load_pvks() -> &'static PVKs { - &VERIFIYING_KEYS -} - -/// Represents an authorization where the Sapling bundle is authorized and the -/// transparent bundle is unauthorized. -pub struct PartialAuthorized; - -impl Authorization for PartialAuthorized { - type SaplingAuth = ::SaplingAuth; - type TransparentAuth = ::TransparentAuth; -} - -/// Partially deauthorize the transparent bundle -pub fn partial_deauthorize( - tx_data: &TransactionData, -) -> Option> { - let transp = tx_data.transparent_bundle().and_then(|x| { - let mut tb = TransparentBuilder::empty(); - for vin in &x.vin { - tb.add_input(TxOut { - asset_type: vin.asset_type, - value: vin.value, - address: vin.address, - }) - .ok()?; - } - for vout in &x.vout { - tb.add_output(&vout.address, vout.asset_type, vout.value) - .ok()?; - } - tb.build() - }); - if tx_data.transparent_bundle().is_some() != transp.is_some() { - return None; - } - Some(TransactionData::from_parts( - tx_data.version(), - tx_data.consensus_branch_id(), - tx_data.lock_time(), - tx_data.expiry_height(), - transp, - tx_data.sapling_bundle().cloned(), - )) -} - -/// Verify a shielded transaction. -pub fn verify_shielded_tx( - transaction: &Transaction, - consume_verify_gas: F, -) -> Result<(), StorageError> -where - F: Fn(u64) -> std::result::Result<(), StorageError>, -{ - tracing::debug!("entered verify_shielded_tx()"); - - let sapling_bundle = if let Some(bundle) = transaction.sapling_bundle() { - bundle - } else { - return Err(StorageError::SimpleMessage("no sapling bundle")); - }; - let tx_data = transaction.deref(); - - // Partially deauthorize the transparent bundle - let unauth_tx_data = match partial_deauthorize(tx_data) { - Some(tx_data) => tx_data, - None => { - return Err(StorageError::SimpleMessage( - "Failed to partially de-authorize", - )); - } - }; - - let txid_parts = unauth_tx_data.digest(TxIdDigester); - // the commitment being signed is shared across all Sapling inputs; once - // V4 transactions are deprecated this should just be the txid, but - // for now we need to continue to compute it here. - let sighash = - signature_hash(&unauth_tx_data, &SignableInput::Shielded, &txid_parts); - tracing::debug!("sighash computed"); - - let PVKs { - spend_vk, - convert_vk, - output_vk, - } = load_pvks(); - - #[cfg(not(feature = "testing"))] - let mut ctx = BatchValidator::new(); - #[cfg(feature = "testing")] - let mut ctx = testing::MockBatchValidator::default(); - - // Charge gas before check bundle - charge_masp_check_bundle_gas(sapling_bundle, &consume_verify_gas)?; - - if !ctx.check_bundle(sapling_bundle.to_owned(), sighash.as_ref().to_owned()) - { - tracing::debug!("failed check bundle"); - return Err(StorageError::SimpleMessage("Invalid sapling bundle")); - } - tracing::debug!("passed check bundle"); - - // Charge gas before final validation - charge_masp_validate_gas(sapling_bundle, consume_verify_gas)?; - if !ctx.validate(spend_vk, convert_vk, output_vk, OsRng) { - return Err(StorageError::SimpleMessage( - "Invalid proofs or signatures", - )); - } - Ok(()) -} - -// Charge gas for the check_bundle operation which does not leverage concurrency -fn charge_masp_check_bundle_gas( - sapling_bundle: &SaplingBundle, - consume_verify_gas: F, -) -> Result<(), namada_state::StorageError> -where - F: Fn(u64) -> std::result::Result<(), namada_state::StorageError>, -{ - consume_verify_gas(checked!( - (sapling_bundle.shielded_spends.len() as u64) - * namada_gas::MASP_SPEND_CHECK_GAS - )?)?; - - consume_verify_gas(checked!( - (sapling_bundle.shielded_converts.len() as u64) - * namada_gas::MASP_CONVERT_CHECK_GAS - )?)?; - - consume_verify_gas(checked!( - (sapling_bundle.shielded_outputs.len() as u64) - * namada_gas::MASP_OUTPUT_CHECK_GAS - )?) -} - -// Charge gas for the final validation, taking advtange of concurrency for -// proofs verification but not for signatures -fn charge_masp_validate_gas( - sapling_bundle: &SaplingBundle, - consume_verify_gas: F, -) -> Result<(), namada_state::StorageError> -where - F: Fn(u64) -> std::result::Result<(), namada_state::StorageError>, -{ - // Signatures gas - consume_verify_gas(checked!( - // Add one for the binding signature - ((sapling_bundle.shielded_spends.len() as u64) + 1) - * namada_gas::MASP_VERIFY_SIG_GAS - )?)?; - - // If at least one note is present charge the fixed costs. Then charge the - // variable cost for every other note, amortized on the fixed expected - // number of cores - if let Some(remaining_notes) = - sapling_bundle.shielded_spends.len().checked_sub(1) - { - consume_verify_gas(namada_gas::MASP_FIXED_SPEND_GAS)?; - consume_verify_gas(checked!( - namada_gas::MASP_VARIABLE_SPEND_GAS * remaining_notes as u64 - / namada_gas::MASP_PARALLEL_GAS_DIVIDER - )?)?; - } - - if let Some(remaining_notes) = - sapling_bundle.shielded_converts.len().checked_sub(1) - { - consume_verify_gas(namada_gas::MASP_FIXED_CONVERT_GAS)?; - consume_verify_gas(checked!( - namada_gas::MASP_VARIABLE_CONVERT_GAS * remaining_notes as u64 - / namada_gas::MASP_PARALLEL_GAS_DIVIDER - )?)?; - } - - if let Some(remaining_notes) = - sapling_bundle.shielded_outputs.len().checked_sub(1) - { - consume_verify_gas(namada_gas::MASP_FIXED_OUTPUT_GAS)?; - consume_verify_gas(checked!( - namada_gas::MASP_VARIABLE_OUTPUT_GAS * remaining_notes as u64 - / namada_gas::MASP_PARALLEL_GAS_DIVIDER - )?)?; - } - - Ok(()) -} - -/// Get the path to MASP parameters from [`ENV_VAR_MASP_PARAMS_DIR`] env var or -/// use the default. -pub fn get_params_dir() -> PathBuf { - if let Ok(params_dir) = env::var(ENV_VAR_MASP_PARAMS_DIR) { - #[allow(clippy::print_stdout)] - { - println!("Using {} as masp parameter folder.", params_dir); - } - PathBuf::from(params_dir) - } else { - masp_proofs::default_params_folder().unwrap() - } -} - /// Freeze a Builder into the format necessary for inclusion in a Tx. This is /// the format used by hardware wallets to validate a MASP Transaction. struct WalletMap; @@ -2025,15 +1748,18 @@ async fn get_indexed_masp_events_at_height( })) } +#[cfg(test)] mod tests { + use masp_proofs::bls12_381::Bls12; + + use super::*; + /// quick and dirty test. will fail on size check #[test] #[should_panic(expected = "parameter file size is not correct")] fn test_wrong_masp_params() { use std::io::Write; - use super::{CONVERT_NAME, OUTPUT_NAME, SPEND_NAME}; - let tempdir = tempfile::tempdir() .expect("expected a temp dir") .into_path(); @@ -2048,7 +1774,7 @@ mod tests { .expect("expected a writable temp file (on sync)"); } - std::env::set_var(super::ENV_VAR_MASP_PARAMS_DIR, tempdir.as_os_str()); + std::env::set_var(ENV_VAR_MASP_PARAMS_DIR, tempdir.as_os_str()); // should panic here masp_proofs::load_parameters( &fake_params_paths[0], @@ -2067,9 +1793,7 @@ mod tests { generate_random_parameters, Parameters, }; use masp_proofs::bellman::{Circuit, ConstraintSystem, SynthesisError}; - use masp_proofs::bls12_381::{Bls12, Scalar}; - - use super::{CONVERT_NAME, OUTPUT_NAME, SPEND_NAME}; + use masp_proofs::bls12_381::Scalar; struct FakeCircuit { x: E, @@ -2126,7 +1850,7 @@ mod tests { .expect("expected a writable temp file (on sync)"); } - std::env::set_var(super::ENV_VAR_MASP_PARAMS_DIR, tempdir.as_os_str()); + std::env::set_var(ENV_VAR_MASP_PARAMS_DIR, tempdir.as_os_str()); // should panic here masp_proofs::load_parameters( &fake_params_paths[0].0, @@ -2142,7 +1866,6 @@ pub mod testing { use std::ops::AddAssign; use std::sync::Mutex; - use bls12_381::{G1Affine, G2Affine}; use masp_primitives::consensus::testing::arb_height; use masp_primitives::constants::SPENDING_KEY_GENERATOR; use masp_primitives::group::GroupEncoding; @@ -2150,9 +1873,10 @@ pub mod testing { use masp_primitives::sapling::redjubjub::{PublicKey, Signature}; use masp_primitives::sapling::{ProofGenerationKey, Rseed}; use masp_primitives::transaction::components::sapling::builder::StoredBuildParams; - use masp_primitives::transaction::components::sapling::Bundle; use masp_primitives::transaction::components::GROTH_PROOF_SIZE; - use masp_proofs::bellman::groth16::{self, Proof}; + use masp_proofs::bellman::groth16::Proof; + use masp_proofs::bls12_381; + use masp_proofs::bls12_381::{Bls12, G1Affine, G2Affine}; use proptest::prelude::*; use proptest::sample::SizeRange; use proptest::test_runner::TestRng; @@ -2168,59 +1892,6 @@ pub mod testing { use crate::masp_primitives::transaction::components::transparent::testing::arb_transparent_address; use crate::token::testing::arb_denomination; - /// A context object for verifying the Sapling components of MASP - /// transactions. Same as BatchValidator, but always assumes the - /// proofs and signatures to be valid. - pub struct MockBatchValidator { - inner: BatchValidator, - } - - impl Default for MockBatchValidator { - fn default() -> Self { - MockBatchValidator { - inner: BatchValidator::new(), - } - } - } - - impl MockBatchValidator { - /// Checks the bundle against Sapling-specific consensus rules, and adds - /// its proof and signatures to the validator. - /// - /// Returns `false` if the bundle doesn't satisfy all of the consensus - /// rules. This `BatchValidator` can continue to be used - /// regardless, but some or all of the proofs and signatures - /// from this bundle may have already been added to the batch even if - /// it fails other consensus rules. - pub fn check_bundle( - &mut self, - bundle: Bundle< - masp_primitives::transaction::components::sapling::Authorized, - >, - sighash: [u8; 32], - ) -> bool { - self.inner.check_bundle(bundle, sighash) - } - - /// Batch-validates the accumulated bundles. - /// - /// Returns `true` if every proof and signature in every bundle added to - /// the batch validator is valid, or `false` if one or more are - /// invalid. No attempt is made to figure out which of the - /// accumulated bundles might be invalid; if that information is - /// desired, construct separate [`BatchValidator`]s for sub-batches of - /// the bundles. - pub fn validate( - self, - _spend_vk: &groth16::VerifyingKey, - _convert_vk: &groth16::VerifyingKey, - _output_vk: &groth16::VerifyingKey, - mut _rng: R, - ) -> bool { - true - } - } - /// This function computes `value` in the exponent of the value commitment /// base fn masp_compute_value_balance( @@ -2944,6 +2615,11 @@ pub mod fs { use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; + use namada_token::validation::{ + get_params_dir, CONVERT_NAME, ENV_VAR_MASP_PARAMS_DIR, OUTPUT_NAME, + SPEND_NAME, + }; + use super::*; /// Shielded context file name diff --git a/crates/shielded_token/Cargo.toml b/crates/shielded_token/Cargo.toml index c9d7618ab9..6f4dac9992 100644 --- a/crates/shielded_token/Cargo.toml +++ b/crates/shielded_token/Cargo.toml @@ -15,17 +15,27 @@ version.workspace = true [features] default = [] multicore = ["dep:rayon"] -testing = ["multicore", "namada_core/testing"] +# Download MASP params if they're not present +download-params = ["masp_proofs/download-params"] +testing = [ + "multicore", + "namada_core/testing", + "masp_primitives/test-dependencies", +] [dependencies] namada_controller = { path = "../controller" } namada_core = { path = "../core" } +namada_gas = { path = "../gas" } namada_parameters = { path = "../parameters" } namada_storage = { path = "../storage" } namada_trans_token = { path = "../trans_token" } borsh.workspace = true +lazy_static.workspace = true masp_primitives.workspace = true +masp_proofs.workspace = true +rand_core.workspace = true rayon = { workspace = true, optional = true } serde.workspace = true smooth-operator.workspace = true diff --git a/crates/shielded_token/src/lib.rs b/crates/shielded_token/src/lib.rs index b586ea2ddd..50d137955c 100644 --- a/crates/shielded_token/src/lib.rs +++ b/crates/shielded_token/src/lib.rs @@ -21,6 +21,7 @@ pub mod conversion; mod storage; pub mod storage_key; pub mod utils; +pub mod validation; use std::str::FromStr; diff --git a/crates/shielded_token/src/validation.rs b/crates/shielded_token/src/validation.rs new file mode 100644 index 0000000000..c52300b4d1 --- /dev/null +++ b/crates/shielded_token/src/validation.rs @@ -0,0 +1,351 @@ +//! MASP verification wrappers. + +use std::env; +use std::ops::Deref; +use std::path::PathBuf; + +use lazy_static::lazy_static; +use masp_primitives::bls12_381::Bls12; +use masp_primitives::transaction::components::sapling::{ + Authorized as SaplingAuthorized, Bundle as SaplingBundle, +}; +use masp_primitives::transaction::components::transparent::builder::TransparentBuilder; +use masp_primitives::transaction::components::TxOut; +use masp_primitives::transaction::sighash::{signature_hash, SignableInput}; +use masp_primitives::transaction::txid::TxIdDigester; +use masp_primitives::transaction::{ + Authorization, Authorized, Transaction, TransactionData, Unauthorized, +}; +use masp_proofs::bellman::groth16::VerifyingKey; +use masp_proofs::sapling::BatchValidator; +use namada_storage::Error; +use rand_core::OsRng; +use smooth_operator::checked; + +// TODO these could be exported from masp_proof crate +/// Spend circuit name +pub const SPEND_NAME: &str = "masp-spend.params"; +/// Output circuit name +pub const OUTPUT_NAME: &str = "masp-output.params"; +/// Convert circuit name +pub const CONVERT_NAME: &str = "masp-convert.params"; + +/// Env var to point to a dir with MASP parameters. When not specified, +/// the default OS specific path is used. +pub const ENV_VAR_MASP_PARAMS_DIR: &str = "NAMADA_MASP_PARAMS_DIR"; + +/// Get the path to MASP parameters from [`ENV_VAR_MASP_PARAMS_DIR`] env var or +/// use the default. +pub fn get_params_dir() -> PathBuf { + if let Ok(params_dir) = env::var(ENV_VAR_MASP_PARAMS_DIR) { + #[allow(clippy::print_stdout)] + { + println!("Using {} as masp parameter folder.", params_dir); + } + PathBuf::from(params_dir) + } else { + masp_proofs::default_params_folder().unwrap() + } +} + +/// Represents an authorization where the Sapling bundle is authorized and the +/// transparent bundle is unauthorized. +pub struct PartialAuthorized; + +impl Authorization for PartialAuthorized { + type SaplingAuth = ::SaplingAuth; + type TransparentAuth = ::TransparentAuth; +} + +/// MASP verifying keys +pub struct PVKs { + /// spend verifying key + pub spend_vk: VerifyingKey, + /// convert verifying key + pub convert_vk: VerifyingKey, + /// output verifying key + pub output_vk: VerifyingKey, +} + +lazy_static! { + /// MASP verifying keys load from parameters + static ref VERIFIYING_KEYS: PVKs = + { + let params_dir = get_params_dir(); + let [spend_path, convert_path, output_path] = + [SPEND_NAME, CONVERT_NAME, OUTPUT_NAME].map(|p| params_dir.join(p)); + + #[cfg(feature = "download-params")] + if !spend_path.exists() || !convert_path.exists() || !output_path.exists() { + let paths = masp_proofs::download_masp_parameters(None).expect( + "MASP parameters were not present, expected the download to \ + succeed", + ); + if paths.spend != spend_path + || paths.convert != convert_path + || paths.output != output_path + { + panic!( + "unrecoverable: downloaded missing masp params, but to an \ + unfamiliar path" + ) + } + } + // size and blake2b checked here + let params = masp_proofs::load_parameters( + spend_path.as_path(), + output_path.as_path(), + convert_path.as_path(), + ); + PVKs { + spend_vk: params.spend_params.vk, + convert_vk: params.convert_params.vk, + output_vk: params.output_params.vk + } + }; +} + +/// Make sure the MASP params are present and load verifying keys into memory +pub fn preload_verifying_keys() -> &'static PVKs { + &VERIFIYING_KEYS +} + +fn load_pvks() -> &'static PVKs { + &VERIFIYING_KEYS +} + +/// Verify a shielded transaction. +pub fn verify_shielded_tx( + transaction: &Transaction, + consume_verify_gas: F, +) -> Result<(), Error> +where + F: Fn(u64) -> std::result::Result<(), Error>, +{ + tracing::debug!("entered verify_shielded_tx()"); + + let sapling_bundle = if let Some(bundle) = transaction.sapling_bundle() { + bundle + } else { + return Err(Error::SimpleMessage("no sapling bundle")); + }; + let tx_data = transaction.deref(); + + // Partially deauthorize the transparent bundle + let unauth_tx_data = match partial_deauthorize(tx_data) { + Some(tx_data) => tx_data, + None => { + return Err(Error::SimpleMessage( + "Failed to partially de-authorize", + )); + } + }; + + let txid_parts = unauth_tx_data.digest(TxIdDigester); + // the commitment being signed is shared across all Sapling inputs; once + // V4 transactions are deprecated this should just be the txid, but + // for now we need to continue to compute it here. + let sighash = + signature_hash(&unauth_tx_data, &SignableInput::Shielded, &txid_parts); + tracing::debug!("sighash computed"); + + let PVKs { + spend_vk, + convert_vk, + output_vk, + } = load_pvks(); + + #[cfg(not(feature = "testing"))] + let mut ctx = BatchValidator::new(); + #[cfg(feature = "testing")] + let mut ctx = testing::MockBatchValidator::default(); + + // Charge gas before check bundle + charge_masp_check_bundle_gas(sapling_bundle, &consume_verify_gas)?; + + if !ctx.check_bundle(sapling_bundle.to_owned(), sighash.as_ref().to_owned()) + { + tracing::debug!("failed check bundle"); + return Err(Error::SimpleMessage("Invalid sapling bundle")); + } + tracing::debug!("passed check bundle"); + + // Charge gas before final validation + charge_masp_validate_gas(sapling_bundle, consume_verify_gas)?; + if !ctx.validate(spend_vk, convert_vk, output_vk, OsRng) { + return Err(Error::SimpleMessage("Invalid proofs or signatures")); + } + Ok(()) +} + +/// Partially deauthorize the transparent bundle +pub fn partial_deauthorize( + tx_data: &TransactionData, +) -> Option> { + let transp = tx_data.transparent_bundle().and_then(|x| { + let mut tb = TransparentBuilder::empty(); + for vin in &x.vin { + tb.add_input(TxOut { + asset_type: vin.asset_type, + value: vin.value, + address: vin.address, + }) + .ok()?; + } + for vout in &x.vout { + tb.add_output(&vout.address, vout.asset_type, vout.value) + .ok()?; + } + tb.build() + }); + if tx_data.transparent_bundle().is_some() != transp.is_some() { + return None; + } + Some(TransactionData::from_parts( + tx_data.version(), + tx_data.consensus_branch_id(), + tx_data.lock_time(), + tx_data.expiry_height(), + transp, + tx_data.sapling_bundle().cloned(), + )) +} + +// Charge gas for the final validation, taking advtange of concurrency for +// proofs verification but not for signatures +fn charge_masp_validate_gas( + sapling_bundle: &SaplingBundle, + consume_verify_gas: F, +) -> Result<(), Error> +where + F: Fn(u64) -> std::result::Result<(), Error>, +{ + // Signatures gas + consume_verify_gas(checked!( + // Add one for the binding signature + ((sapling_bundle.shielded_spends.len() as u64) + 1) + * namada_gas::MASP_VERIFY_SIG_GAS + )?)?; + + // If at least one note is present charge the fixed costs. Then charge the + // variable cost for every other note, amortized on the fixed expected + // number of cores + if let Some(remaining_notes) = + sapling_bundle.shielded_spends.len().checked_sub(1) + { + consume_verify_gas(namada_gas::MASP_FIXED_SPEND_GAS)?; + consume_verify_gas(checked!( + namada_gas::MASP_VARIABLE_SPEND_GAS * remaining_notes as u64 + / namada_gas::MASP_PARALLEL_GAS_DIVIDER + )?)?; + } + + if let Some(remaining_notes) = + sapling_bundle.shielded_converts.len().checked_sub(1) + { + consume_verify_gas(namada_gas::MASP_FIXED_CONVERT_GAS)?; + consume_verify_gas(checked!( + namada_gas::MASP_VARIABLE_CONVERT_GAS * remaining_notes as u64 + / namada_gas::MASP_PARALLEL_GAS_DIVIDER + )?)?; + } + + if let Some(remaining_notes) = + sapling_bundle.shielded_outputs.len().checked_sub(1) + { + consume_verify_gas(namada_gas::MASP_FIXED_OUTPUT_GAS)?; + consume_verify_gas(checked!( + namada_gas::MASP_VARIABLE_OUTPUT_GAS * remaining_notes as u64 + / namada_gas::MASP_PARALLEL_GAS_DIVIDER + )?)?; + } + + Ok(()) +} + +// Charge gas for the check_bundle operation which does not leverage concurrency +fn charge_masp_check_bundle_gas( + sapling_bundle: &SaplingBundle, + consume_verify_gas: F, +) -> Result<(), Error> +where + F: Fn(u64) -> std::result::Result<(), Error>, +{ + consume_verify_gas(checked!( + (sapling_bundle.shielded_spends.len() as u64) + * namada_gas::MASP_SPEND_CHECK_GAS + )?)?; + + consume_verify_gas(checked!( + (sapling_bundle.shielded_converts.len() as u64) + * namada_gas::MASP_CONVERT_CHECK_GAS + )?)?; + + consume_verify_gas(checked!( + (sapling_bundle.shielded_outputs.len() as u64) + * namada_gas::MASP_OUTPUT_CHECK_GAS + )?) +} + +#[cfg(any(test, feature = "testing"))] +/// Tests and strategies for transactions +pub mod testing { + use masp_primitives::transaction::components::sapling::Bundle; + use masp_proofs::bellman::groth16; + use rand_core::{CryptoRng, RngCore}; + + use super::*; + + /// A context object for verifying the Sapling components of MASP + /// transactions. Same as BatchValidator, but always assumes the + /// proofs and signatures to be valid. + pub struct MockBatchValidator { + inner: BatchValidator, + } + + impl Default for MockBatchValidator { + fn default() -> Self { + MockBatchValidator { + inner: BatchValidator::new(), + } + } + } + + impl MockBatchValidator { + /// Checks the bundle against Sapling-specific consensus rules, and adds + /// its proof and signatures to the validator. + /// + /// Returns `false` if the bundle doesn't satisfy all of the consensus + /// rules. This `BatchValidator` can continue to be used + /// regardless, but some or all of the proofs and signatures + /// from this bundle may have already been added to the batch even if + /// it fails other consensus rules. + pub fn check_bundle( + &mut self, + bundle: Bundle< + masp_primitives::transaction::components::sapling::Authorized, + >, + sighash: [u8; 32], + ) -> bool { + self.inner.check_bundle(bundle, sighash) + } + + /// Batch-validates the accumulated bundles. + /// + /// Returns `true` if every proof and signature in every bundle added to + /// the batch validator is valid, or `false` if one or more are + /// invalid. No attempt is made to figure out which of the + /// accumulated bundles might be invalid; if that information is + /// desired, construct separate [`BatchValidator`]s for sub-batches of + /// the bundles. + pub fn validate( + self, + _spend_vk: &groth16::VerifyingKey, + _convert_vk: &groth16::VerifyingKey, + _output_vk: &groth16::VerifyingKey, + mut _rng: R, + ) -> bool { + true + } + } +} diff --git a/crates/token/Cargo.toml b/crates/token/Cargo.toml index d944683801..175f73b54a 100644 --- a/crates/token/Cargo.toml +++ b/crates/token/Cargo.toml @@ -15,7 +15,8 @@ version.workspace = true [features] default = [] multicore = ["namada_shielded_token/multicore"] -testing = ["namada_core/testing", "proptest"] +download-params = ["namada_shielded_token/download-params"] +testing = ["namada_core/testing", "namada_shielded_token/testing", "proptest"] [dependencies] namada_core = { path = "../core" } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 7c491ea3d2..2b4edd180c 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3977,7 +3977,6 @@ version = "0.39.0" dependencies = [ "async-trait", "bimap", - "bls12_381", "borsh 1.4.0", "borsh-ext", "circular-queue", @@ -4044,12 +4043,17 @@ name = "namada_shielded_token" version = "0.39.0" dependencies = [ "borsh 1.4.0", + "lazy_static", "masp_primitives", + "masp_proofs", "namada_controller", "namada_core", + "namada_gas", "namada_parameters", "namada_storage", "namada_trans_token", + "rand_core 0.6.4", + "rayon", "serde", "smooth-operator", "tracing", diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index 44008f9c09..da515baa6a 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -4033,7 +4033,6 @@ version = "0.39.0" dependencies = [ "async-trait", "bimap", - "bls12_381", "borsh 1.2.1", "borsh-ext", "circular-queue", @@ -4098,12 +4097,17 @@ name = "namada_shielded_token" version = "0.39.0" dependencies = [ "borsh 1.2.1", + "lazy_static", "masp_primitives", + "masp_proofs", "namada_controller", "namada_core", + "namada_gas", "namada_parameters", "namada_storage", "namada_trans_token", + "rand_core 0.6.4", + "rayon", "serde", "smooth-operator", "tracing", From 4f03fc3a2dd33323270e632a10ceac4511b9a8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 17 Jun 2024 16:30:01 +0100 Subject: [PATCH 2/3] shielded_token: feature guard validation to avoid compilation into wasm --- Cargo.lock | 6 +++--- Cargo.toml | 4 ++-- crates/encoding_spec/Cargo.toml | 2 +- crates/namada/Cargo.toml | 2 -- crates/sdk/Cargo.toml | 4 ++-- crates/shielded_token/Cargo.toml | 5 ++--- wasm/Cargo.lock | 6 +++--- wasm_for_tests/Cargo.lock | 6 +++--- 8 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c12594851..b51c42efd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4448,7 +4448,7 @@ dependencies = [ [[package]] name = "masp_note_encryption" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "borsh 1.2.1", "chacha20", @@ -4461,7 +4461,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "aes", "bip0039", @@ -4493,7 +4493,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "bellman", "blake2b_simd", diff --git a/Cargo.toml b/Cargo.toml index d9ce2699f1..5d883b5e01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,8 +124,8 @@ libc = "0.2.97" libloading = "0.7.2" linkme = "0.3.24" # branch = "main" -masp_primitives = { git = "https://github.com/anoma/masp", rev = "4ede1c42d76d6348af8224bc8bfac4404321fe82" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "4ede1c42d76d6348af8224bc8bfac4404321fe82", default-features = false, features = ["local-prover"] } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "71b36beba21e35f8611f3ba6910ed5b78ec11af1" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "71b36beba21e35f8611f3ba6910ed5b78ec11af1", default-features = false, features = ["local-prover"] } num256 = "0.3.5" num_cpus = "1.13.0" num-derive = "0.4" diff --git a/crates/encoding_spec/Cargo.toml b/crates/encoding_spec/Cargo.toml index 487b7916da..519f0f8103 100644 --- a/crates/encoding_spec/Cargo.toml +++ b/crates/encoding_spec/Cargo.toml @@ -17,7 +17,7 @@ default = [] namada-eth-bridge = ["namada/namada-eth-bridge"] [dependencies] -namada = { path = "../namada", features = ["rand", "tendermint-rpc", "download-params"] } +namada = { path = "../namada", features = ["rand", "tendermint-rpc"] } borsh.workspace = true itertools.workspace = true lazy_static.workspace = true diff --git a/crates/namada/Cargo.toml b/crates/namada/Cargo.toml index a2f4ba22f1..d598457231 100644 --- a/crates/namada/Cargo.toml +++ b/crates/namada/Cargo.toml @@ -73,8 +73,6 @@ multicore = [ "namada_sdk/multicore", "namada_token/multicore", ] -# Download MASP params if they're not present -download-params = ["namada_sdk/download-params"] rand = ["namada_sdk/rand"] migrations = [ "namada_migrations", diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 64faf3d0bd..d1632ffe97 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -15,7 +15,7 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["tendermint-rpc", "download-params", "std", "rand", "migrations"] +default = ["tendermint-rpc", "std", "rand", "migrations"] mainnet = ["namada_core/mainnet", "namada_events/mainnet"] @@ -23,7 +23,7 @@ multicore = ["masp_proofs/multicore"] namada-sdk = ["tendermint-rpc", "masp_primitives/transparent-inputs"] -std = ["fd-lock"] +std = ["fd-lock", "download-params"] rand = ["dep:rand", "rand_core", "namada_core/rand"] # tendermint-rpc support diff --git a/crates/shielded_token/Cargo.toml b/crates/shielded_token/Cargo.toml index 6f4dac9992..63a808b625 100644 --- a/crates/shielded_token/Cargo.toml +++ b/crates/shielded_token/Cargo.toml @@ -15,13 +15,12 @@ version.workspace = true [features] default = [] multicore = ["dep:rayon"] -# Download MASP params if they're not present -download-params = ["masp_proofs/download-params"] testing = [ "multicore", "namada_core/testing", "masp_primitives/test-dependencies", ] +download-params = ["masp_proofs/download-params"] [dependencies] namada_controller = { path = "../controller" } @@ -34,7 +33,7 @@ namada_trans_token = { path = "../trans_token" } borsh.workspace = true lazy_static.workspace = true masp_primitives.workspace = true -masp_proofs.workspace = true +masp_proofs = { workspace = true } rand_core.workspace = true rayon = { workspace = true, optional = true } serde.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 2b4edd180c..4f835ec4ec 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3487,7 +3487,7 @@ dependencies = [ [[package]] name = "masp_note_encryption" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "borsh 1.4.0", "chacha20", @@ -3500,7 +3500,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "aes", "bip0039", @@ -3532,7 +3532,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "bellman", "blake2b_simd", diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index da515baa6a..d9f0f36d93 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -3568,7 +3568,7 @@ dependencies = [ [[package]] name = "masp_note_encryption" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "borsh 1.2.1", "chacha20", @@ -3581,7 +3581,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "aes", "bip0039", @@ -3613,7 +3613,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "1.0.0" -source = "git+https://github.com/anoma/masp?rev=4ede1c42d76d6348af8224bc8bfac4404321fe82#4ede1c42d76d6348af8224bc8bfac4404321fe82" +source = "git+https://github.com/anoma/masp?rev=71b36beba21e35f8611f3ba6910ed5b78ec11af1#71b36beba21e35f8611f3ba6910ed5b78ec11af1" dependencies = [ "bellman", "blake2b_simd", From d6212b95967f1d204ef3f61b4ab2937ed3ad0d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 17 Jun 2024 16:31:29 +0100 Subject: [PATCH 3/3] changelog: add #3419 --- .changelog/unreleased/improvements/3419-move-verify-shielded.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/3419-move-verify-shielded.md diff --git a/.changelog/unreleased/improvements/3419-move-verify-shielded.md b/.changelog/unreleased/improvements/3419-move-verify-shielded.md new file mode 100644 index 0000000000..fa3576d0e9 --- /dev/null +++ b/.changelog/unreleased/improvements/3419-move-verify-shielded.md @@ -0,0 +1,2 @@ +- Moved shielded tx validation out of the SDK crate into shielded token crate. + ([\#3419](https://github.com/anoma/namada/pull/3419)) \ No newline at end of file