use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::marker::PhantomData;
use borsh::BorshDeserialize;
use masp_primitives::asset_type::AssetType;
use masp_primitives::merkle_tree::CommitmentTree;
use masp_primitives::sapling::Node;
use masp_primitives::transaction::components::transparent::Authorization;
use masp_primitives::transaction::components::{
I128Sum, TxIn, TxOut, ValueSum,
};
use masp_primitives::transaction::{Transaction, TransparentAddress};
use namada_core::address::{self, Address};
use namada_core::arith::{CheckedAdd, CheckedSub, checked};
use namada_core::booleans::BoolResultUnitExt;
use namada_core::collections::HashSet;
use namada_core::masp::{MaspEpoch, TAddrData, addr_taddr, encode_asset_type};
use namada_core::storage::Key;
use namada_core::token;
use namada_core::token::{Amount, MaspDigitPos};
use namada_core::uint::I320;
use namada_state::{
ConversionState, OptionExt, ReadConversionState, ResultExt,
};
use namada_systems::{governance, ibc, parameters, trans_token};
use namada_tx::BatchedTxRef;
use namada_vp_env::{Error, Result, VpEnv};
use crate::storage_key::{
is_masp_key, is_masp_nullifier_key, is_masp_transfer_key,
is_masp_undated_balance_key, masp_commitment_anchor_key,
masp_commitment_tree_key, masp_convert_anchor_key, masp_nullifier_key,
masp_undated_balance_key,
};
use crate::validation::verify_shielded_tx;
pub struct MaspVp<'ctx, CTX, Params, Gov, Ibc, TransToken, Transfer> {
pub _marker:
PhantomData<(&'ctx CTX, Params, Gov, Ibc, TransToken, Transfer)>,
}
#[derive(Debug, Clone)]
struct ChangedBalances {
undated_tokens:
BTreeMap<AssetType, (Address, token::Denomination, MaspDigitPos)>,
decoder: BTreeMap<TransparentAddress, TAddrData>,
pre: BTreeMap<TransparentAddress, ValueSum<Address, Amount>>,
post: BTreeMap<TransparentAddress, ValueSum<Address, Amount>>,
undated_pre: ValueSum<Address, Amount>,
undated_post: ValueSum<Address, Amount>,
}
impl Default for ChangedBalances {
fn default() -> Self {
Self {
undated_tokens: Default::default(),
decoder: Default::default(),
pre: Default::default(),
post: Default::default(),
undated_pre: ValueSum::zero(),
undated_post: ValueSum::zero(),
}
}
}
impl<'ctx, CTX, Params, Gov, Ibc, TransToken, Transfer>
MaspVp<'ctx, CTX, Params, Gov, Ibc, TransToken, Transfer>
where
CTX: VpEnv<'ctx>
+ namada_tx::action::Read<Err = Error>
+ ReadConversionState,
Params: parameters::Read<<CTX as VpEnv<'ctx>>::Pre>,
Gov: governance::Read<<CTX as VpEnv<'ctx>>::Pre>,
Ibc: ibc::Read<<CTX as VpEnv<'ctx>>::Post>,
TransToken:
trans_token::Keys + trans_token::Read<<CTX as VpEnv<'ctx>>::Pre>,
Transfer: BorshDeserialize,
{
pub fn validate_tx(
ctx: &'ctx CTX,
tx_data: &BatchedTxRef<'_>,
keys_changed: &BTreeSet<Key>,
verifiers: &BTreeSet<Address>,
) -> Result<()> {
if Gov::is_proposal_accepted(
&ctx.pre(),
tx_data.tx.data(tx_data.cmt).unwrap_or_default().as_ref(),
)? {
return Ok(());
}
let masp_keys_changed: Vec<&Key> =
keys_changed.iter().filter(|key| is_masp_key(key)).collect();
let masp_transfer_changes = masp_keys_changed
.iter()
.all(|key| is_masp_transfer_key(key));
if masp_keys_changed.is_empty() {
Ok(())
} else if masp_transfer_changes {
Self::is_valid_masp_transfer(ctx, tx_data, keys_changed, verifiers)
} else {
return Err(Error::new_const(
"A governance proposal is required to modify MASP \
non-transfer keys",
));
}
}
fn valid_nullifiers_reveal(
ctx: &'ctx CTX,
keys_changed: &BTreeSet<Key>,
transaction: &Transaction,
) -> Result<()> {
let mut revealed_nullifiers = HashSet::new();
for description in transaction
.sapling_bundle()
.map_or(&vec![], |bundle| &bundle.shielded_spends)
{
let nullifier_key = masp_nullifier_key(&description.nullifier);
if ctx.has_key_pre(&nullifier_key)?
|| revealed_nullifiers.contains(&nullifier_key)
{
let error = Error::new_alloc(format!(
"MASP double spending attempt, the nullifier {:?} has \
already been revealed previously",
description.nullifier.0,
));
tracing::debug!("{error}");
return Err(error);
}
ctx.read_bytes_post(&nullifier_key)?
.is_some_and(|value| value.is_empty())
.ok_or_else(|| {
Error::new_const(
"The nullifier should have been committed with no \
associated data",
)
})?;
revealed_nullifiers.insert(nullifier_key);
}
for nullifier_key in
keys_changed.iter().filter(|key| is_masp_nullifier_key(key))
{
if !revealed_nullifiers.contains(nullifier_key) {
let error = Error::new_alloc(format!(
"An unexpected MASP nullifier key {nullifier_key} has \
been revealed by the transaction"
));
tracing::debug!("{error}");
return Err(error);
}
}
Ok(())
}
fn apply_undated_balances(
ctx: &'ctx CTX,
keys_changed: &BTreeSet<Key>,
mut result: ChangedBalances,
) -> Result<ChangedBalances> {
for token in keys_changed.iter().filter_map(is_masp_undated_balance_key)
{
let pre_undated_balance: Amount = ctx
.read_pre(&masp_undated_balance_key(&token))?
.unwrap_or_default();
let pre_undated_balance =
ValueSum::from_pair(token.clone(), pre_undated_balance);
result.undated_pre =
checked!(result.undated_pre.clone() + &pre_undated_balance)
.map_err(Error::new)?;
let post_undated_balance: Amount = ctx
.read_post(&masp_undated_balance_key(&token))?
.unwrap_or_default();
let post_undated_balance =
ValueSum::from_pair(token, post_undated_balance);
result.undated_post =
checked!(result.undated_post.clone() + &post_undated_balance)
.map_err(Error::new)?;
}
Ok(result)
}
fn valid_note_commitment_update(
ctx: &'ctx CTX,
transaction: &Transaction,
) -> Result<()> {
let tree_key = masp_commitment_tree_key();
let mut previous_tree: CommitmentTree<Node> = ctx
.read_pre(&tree_key)?
.ok_or(Error::new_const("Cannot read storage"))?;
let post_tree: CommitmentTree<Node> = ctx
.read_post(&tree_key)?
.ok_or(Error::new_const("Cannot read storage"))?;
for description in transaction
.sapling_bundle()
.map_or(&vec![], |bundle| &bundle.shielded_outputs)
{
previous_tree
.append(Node::from_scalar(description.cmu))
.map_err(|()| {
Error::new_const("Failed to update the commitment tree")
})?;
}
if previous_tree != post_tree {
let error = Error::new_const(
"The note commitment tree was incorrectly updated",
);
tracing::debug!("{error}");
return Err(error);
}
Ok(())
}
fn valid_spend_descriptions_anchor(
ctx: &'ctx CTX,
transaction: &Transaction,
) -> Result<()> {
for description in transaction
.sapling_bundle()
.map_or(&vec![], |bundle| &bundle.shielded_spends)
{
let anchor_key = masp_commitment_anchor_key(description.anchor);
if !ctx.has_key_pre(&anchor_key)? {
let error = Error::new_const(
"Spend description refers to an invalid anchor",
);
tracing::debug!("{error}");
return Err(error);
}
}
Ok(())
}
fn valid_convert_descriptions_anchor(
ctx: &'ctx CTX,
transaction: &Transaction,
) -> Result<()> {
if let Some(bundle) = transaction.sapling_bundle() {
if !bundle.shielded_converts.is_empty() {
let anchor_key = masp_convert_anchor_key();
let expected_anchor = ctx
.read_pre::<namada_core::hash::Hash>(&anchor_key)?
.ok_or(Error::new_const("Cannot read storage"))?;
for description in &bundle.shielded_converts {
if namada_core::hash::Hash(description.anchor.to_bytes())
!= expected_anchor
{
let error = Error::new_const(
"Convert description refers to an invalid anchor",
);
tracing::debug!("{error}");
return Err(error);
}
}
}
}
Ok(())
}
fn apply_balance_change(
ctx: &'ctx CTX,
mut result: ChangedBalances,
[token, counterpart]: [&Address; 2],
) -> Result<ChangedBalances> {
let denom = TransToken::read_denom(&ctx.pre(), token)?.ok_or_err_msg(
"No denomination found in storage for the given token",
)?;
undated_tokens(token, denom, &mut result.undated_tokens)?;
let counterpart_balance_key =
TransToken::balance_key(token, counterpart);
let pre_balance: Amount =
ctx.read_pre(&counterpart_balance_key)?.unwrap_or_default();
let post_balance: Amount =
ctx.read_post(&counterpart_balance_key)?.unwrap_or_default();
let addr_hash = addr_taddr(counterpart.clone());
result
.decoder
.insert(addr_hash, TAddrData::Addr(counterpart.clone()));
let zero = ValueSum::zero();
let pre_entry = result.pre.get(&addr_hash).unwrap_or(&zero).clone();
result.pre.insert(
addr_hash,
checked!(
pre_entry + &ValueSum::from_pair((*token).clone(), pre_balance)
)
.map_err(Error::new)?,
);
let post_entry = result.post.get(&addr_hash).cloned().unwrap_or(zero);
result.post.insert(
addr_hash,
checked!(
post_entry
+ &ValueSum::from_pair((*token).clone(), post_balance)
)
.map_err(Error::new)?,
);
Result::<_>::Ok(result)
}
fn validate_state_and_get_transfer_data(
ctx: &'ctx CTX,
keys_changed: &BTreeSet<Key>,
tx_data: &[u8],
) -> Result<ChangedBalances> {
let mut counterparts_balances = keys_changed
.iter()
.filter_map(TransToken::is_any_token_balance_key);
let changed_balances = counterparts_balances
.try_fold(ChangedBalances::default(), |acc, account| {
Self::apply_balance_change(ctx, acc, account)
})?;
let mut changed_balances =
Self::apply_undated_balances(ctx, keys_changed, changed_balances)?;
let ibc_addr = TAddrData::Addr(address::IBC);
changed_balances
.decoder
.insert(addr_taddr(address::IBC), ibc_addr);
let ChangedBalances {
undated_tokens,
decoder,
pre,
post,
undated_pre,
undated_post,
} = changed_balances;
let ibc::ChangedBalances { decoder, pre, post } =
Ibc::apply_ibc_packet::<Transfer>(
&ctx.post(),
tx_data,
ibc::ChangedBalances { decoder, pre, post },
keys_changed,
)?;
Ok(ChangedBalances {
undated_tokens,
decoder,
pre,
post,
undated_pre,
undated_post,
})
}
fn is_valid_masp_transfer(
ctx: &'ctx CTX,
batched_tx: &BatchedTxRef<'_>,
keys_changed: &BTreeSet<Key>,
verifiers: &BTreeSet<Address>,
) -> Result<()> {
let masp_epoch_multiplier = Params::masp_epoch_multiplier(&ctx.pre())?;
let masp_epoch = MaspEpoch::try_from_epoch(
ctx.get_block_epoch()?,
masp_epoch_multiplier,
)
.map_err(Error::new_const)?;
let conversion_state = ctx.conversion_state();
let tx_data = batched_tx
.tx
.data(batched_tx.cmt)
.ok_or_err_msg("No transaction data")?;
let actions = ctx.read_actions()?;
let shielded_tx = if let Some(tx) =
Ibc::try_extract_masp_tx_from_envelope::<Transfer>(&tx_data)?
{
tx
} else {
let masp_section_ref =
namada_tx::action::get_masp_section_ref(&actions)
.map_err(Error::new_const)?
.ok_or_else(|| {
Error::new_const(
"Missing MASP section reference in action",
)
})?;
batched_tx
.tx
.get_masp_section(&masp_section_ref)
.cloned()
.ok_or_else(|| {
Error::new_const("Missing MASP section in transaction")
})?
};
if u64::from(ctx.get_block_height()?)
> u64::from(shielded_tx.expiry_height())
{
let error = Error::new_const("MASP transaction is expired");
tracing::debug!("{error}");
return Err(error);
}
let changed_balances = Self::validate_state_and_get_transfer_data(
ctx,
keys_changed,
&tx_data,
)?;
let zero = ValueSum::zero();
let masp_address_hash = addr_taddr(address::MASP);
verify_sapling_balancing_value(
changed_balances
.pre
.get(&masp_address_hash)
.unwrap_or(&zero),
changed_balances
.post
.get(&masp_address_hash)
.unwrap_or(&zero),
&changed_balances.undated_pre,
&changed_balances.undated_post,
&shielded_tx.sapling_value_balance(),
masp_epoch,
&changed_balances.undated_tokens,
conversion_state,
)?;
let mut authorizers = BTreeSet::new();
Self::valid_spend_descriptions_anchor(ctx, &shielded_tx)?;
Self::valid_convert_descriptions_anchor(ctx, &shielded_tx)?;
Self::valid_nullifiers_reveal(ctx, keys_changed, &shielded_tx)?;
Self::valid_note_commitment_update(ctx, &shielded_tx)?;
let mut changed_bals_minus_txn = changed_balances.clone();
validate_transparent_bundle(
&shielded_tx,
&mut changed_bals_minus_txn,
masp_epoch,
conversion_state,
&mut authorizers,
)?;
for (addr, minus_txn_pre) in changed_bals_minus_txn.pre {
let pre = changed_balances.pre.get(&addr).unwrap_or(&zero);
let post = changed_balances.post.get(&addr).unwrap_or(&zero);
let minus_txn_post =
changed_bals_minus_txn.post.get(&addr).unwrap_or(&zero);
if addr != masp_address_hash &&
minus_txn_post < &minus_txn_pre &&
(minus_txn_pre, minus_txn_post) != (pre.clone(), post)
{
authorizers.insert(addr);
}
}
let mut actions_authorizers: HashSet<&Address> = actions
.iter()
.filter_map(|action| {
if let namada_tx::action::Action::Masp(
namada_tx::action::MaspAction::MaspAuthorizer(addr),
) = action
{
Some(addr)
} else {
None
}
})
.collect();
for authorizer in authorizers {
if let Some(TAddrData::Addr(address::IBC)) =
changed_bals_minus_txn.decoder.get(&authorizer)
{
if let Some(transp_bundle) = shielded_tx.transparent_bundle() {
for vout in transp_bundle.vout.iter() {
if let Some(TAddrData::Ibc(_)) =
changed_bals_minus_txn.decoder.get(&vout.address)
{
let error = Error::new_const(
"Simultaneous credit and debit of IBC account \
in a MASP transaction not allowed",
);
tracing::debug!("{error}");
return Err(error);
}
}
}
} else if let Some(TAddrData::Addr(signer)) =
changed_bals_minus_txn.decoder.get(&authorizer)
{
if !verifiers.contains(signer) {
let error = Error::new_alloc(format!(
"The required vp of address {signer} was not triggered"
));
tracing::debug!("{error}");
return Err(error);
}
if !actions_authorizers.swap_remove(signer) {
let error = Error::new_alloc(format!(
"The required masp authorizer action for address \
{signer} is missing"
));
tracing::debug!("{error}");
return Err(error);
}
} else {
let error = Error::new_const(
"Unable to decode a transaction authorizer",
);
tracing::debug!("{error}");
return Err(error);
}
}
if !actions_authorizers.is_empty() {
let error = Error::new_const(
"Found masp authorizer actions that are not required",
);
tracing::debug!("{error}");
return Err(error);
}
verify_shielded_tx(&shielded_tx, |gas| ctx.charge_gas(gas))
}
}
fn undated_tokens(
token: &Address,
denom: token::Denomination,
tokens: &mut BTreeMap<
AssetType,
(Address, token::Denomination, MaspDigitPos),
>,
) -> Result<()> {
for digit in MaspDigitPos::iter() {
let asset_type = encode_asset_type(token.clone(), denom, digit, None)
.wrap_err("unable to create asset type")?;
tokens.insert(asset_type, (token.clone(), denom, digit));
}
Ok(())
}
fn validate_transparent_input<A: Authorization>(
vin: &TxIn<A>,
changed_balances: &mut ChangedBalances,
transparent_tx_pool: &mut I128Sum,
epoch: MaspEpoch,
conversion_state: &ConversionState,
authorizers: &mut BTreeSet<TransparentAddress>,
) -> Result<()> {
authorizers.insert(vin.address);
*transparent_tx_pool = transparent_tx_pool
.checked_add(
&I128Sum::from_nonnegative(vin.asset_type, i128::from(vin.value))
.ok()
.ok_or_err_msg("invalid value or asset type for amount")?,
)
.ok_or_err_msg("Overflow in input sum")?;
let bal_ref = changed_balances
.pre
.entry(vin.address)
.or_insert(ValueSum::zero());
match conversion_state.assets.get(&vin.asset_type) {
Some(asset) if asset.epoch == epoch => {
let amount = token::Amount::from_masp_denominated(
vin.value,
asset.digit_pos,
);
*bal_ref = bal_ref
.checked_sub(&ValueSum::from_pair(asset.token.clone(), amount))
.ok_or_else(|| {
Error::new_const("Underflow in bundle balance")
})?;
}
None if changed_balances
.undated_tokens
.contains_key(&vin.asset_type) =>
{
let (token, denom, digit) =
&changed_balances.undated_tokens[&vin.asset_type];
let dated_asset_type =
encode_asset_type(token.clone(), *denom, *digit, Some(epoch))
.wrap_err("unable to create asset type")?;
if conversion_state.assets.contains_key(&dated_asset_type) {
let error =
Error::new_const("epoch is missing from asset type");
tracing::debug!("{error}");
return Err(error);
} else {
let amount =
token::Amount::from_masp_denominated(vin.value, *digit);
*bal_ref = bal_ref
.checked_sub(&ValueSum::from_pair(token.clone(), amount))
.ok_or_else(|| {
Error::new_const("Underflow in bundle balance")
})?;
}
}
_ => {
let error = Error::new_const("Unable to decode asset type");
tracing::debug!("{error}");
return Err(error);
}
};
Ok(())
}
fn validate_transparent_output(
out: &TxOut,
changed_balances: &mut ChangedBalances,
transparent_tx_pool: &mut I128Sum,
epoch: MaspEpoch,
conversion_state: &ConversionState,
) -> Result<()> {
*transparent_tx_pool = transparent_tx_pool
.checked_sub(
&I128Sum::from_nonnegative(out.asset_type, i128::from(out.value))
.ok()
.ok_or_err_msg("invalid value or asset type for amount")?,
)
.ok_or_err_msg("Underflow in output subtraction")?;
let bal_ref = changed_balances
.post
.entry(out.address)
.or_insert(ValueSum::zero());
match conversion_state.assets.get(&out.asset_type) {
Some(asset) if asset.epoch <= epoch => {
let amount = token::Amount::from_masp_denominated(
out.value,
asset.digit_pos,
);
*bal_ref = bal_ref
.checked_sub(&ValueSum::from_pair(asset.token.clone(), amount))
.ok_or_else(|| {
Error::new_const("Underflow in bundle balance")
})?;
}
None if changed_balances
.undated_tokens
.contains_key(&out.asset_type) =>
{
let (token, _denom, digit) =
&changed_balances.undated_tokens[&out.asset_type];
let amount =
token::Amount::from_masp_denominated(out.value, *digit);
*bal_ref = bal_ref
.checked_sub(&ValueSum::from_pair(token.clone(), amount))
.ok_or_else(|| {
Error::new_const("Underflow in bundle balance")
})?;
}
_ => {
let error = Error::new_const("Unable to decode asset type");
tracing::debug!("{error}");
return Err(error);
}
};
Ok(())
}
fn validate_transparent_bundle(
shielded_tx: &Transaction,
changed_balances: &mut ChangedBalances,
epoch: MaspEpoch,
conversion_state: &ConversionState,
authorizers: &mut BTreeSet<TransparentAddress>,
) -> Result<()> {
let mut transparent_tx_pool = shielded_tx.sapling_value_balance();
if let Some(transp_bundle) = shielded_tx.transparent_bundle() {
for vin in transp_bundle.vin.iter() {
validate_transparent_input(
vin,
changed_balances,
&mut transparent_tx_pool,
epoch,
conversion_state,
authorizers,
)?;
}
for out in transp_bundle.vout.iter() {
validate_transparent_output(
out,
changed_balances,
&mut transparent_tx_pool,
epoch,
conversion_state,
)?;
}
}
match transparent_tx_pool.partial_cmp(&I128Sum::zero()) {
None | Some(Ordering::Less) => {
let error = Error::new_const(
"Transparent transaction value pool must be nonnegative. \
Violation may be caused by transaction being constructed in \
previous epoch. Maybe try again.",
);
tracing::debug!("{error}");
Err(error)
}
Some(Ordering::Greater) => {
let error = Error::new_const(
"Transaction fees cannot be left on the MASP balance.",
);
tracing::debug!("{error}");
Err(error)
}
_ => Ok(()),
}
}
fn apply_balance_component(
acc: &ValueSum<Address, I320>,
val: i128,
digit: MaspDigitPos,
address: Address,
) -> Result<ValueSum<Address, I320>> {
let decoded_change = I320::from_masp_denominated(val, digit)
.map_err(|_| Error::new_const("Overflow in MASP value balance"))?;
let decoded_change = ValueSum::from_pair(address, decoded_change);
acc.checked_add(&decoded_change)
.ok_or_else(|| Error::new_const("Overflow in MASP value balance"))
}
#[allow(clippy::too_many_arguments)]
fn verify_sapling_balancing_value(
pre: &ValueSum<Address, Amount>,
post: &ValueSum<Address, Amount>,
undated_pre: &ValueSum<Address, Amount>,
undated_post: &ValueSum<Address, Amount>,
sapling_value_balance: &I128Sum,
target_epoch: MaspEpoch,
tokens: &BTreeMap<AssetType, (Address, token::Denomination, MaspDigitPos)>,
conversion_state: &ConversionState,
) -> Result<()> {
let mut acc = ValueSum::<Address, I320>::from_sum(post.clone());
let mut undated_acc =
ValueSum::<Address, I320>::from_sum(undated_post.clone());
for (asset_type, val) in sapling_value_balance.components() {
match conversion_state.assets.get(asset_type) {
Some(asset) if asset.epoch <= target_epoch => {
acc = apply_balance_component(
&acc,
*val,
asset.digit_pos,
asset.token.clone(),
)?;
}
None if tokens.contains_key(asset_type) => {
let (token, _denom, digit) = &tokens[asset_type];
acc =
apply_balance_component(&acc, *val, *digit, token.clone())?;
undated_acc = apply_balance_component(
&undated_acc,
*val,
*digit,
token.clone(),
)?;
}
_ => {
let error = Error::new_const("Unable to decode asset type");
tracing::debug!("{error}");
return Err(error);
}
}
}
if acc != ValueSum::from_sum(pre.clone()) {
let error = Error::new_const(
"MASP balance change not equal to Sapling value balance",
);
tracing::debug!("{error}");
Err(error)
} else if undated_acc != ValueSum::from_sum(undated_pre.clone()) {
let error = Error::new_const(
"MASP undated balance change not equal to undated Sapling value \
balance",
);
tracing::debug!("{error}");
return Err(error);
} else {
Ok(())
}
}
#[cfg(test)]
mod shielded_token_tests {
use std::cell::RefCell;
use std::collections::BTreeSet;
use namada_core::address::MASP;
use namada_core::address::testing::nam;
use namada_core::borsh::BorshSerializeExt;
use namada_gas::{TxGasMeter, VpGasMeter};
use namada_state::testing::{TestState, arb_account_storage_key, arb_key};
use namada_state::{StateRead, TxIndex};
use namada_trans_token::Amount;
use namada_trans_token::storage_key::balance_key;
use namada_tx::{BatchedTx, Tx};
use namada_vm::WasmCacheRwAccess;
use namada_vm::wasm::VpCache;
use namada_vm::wasm::compilation_cache::common::testing::vp_cache;
use namada_vm::wasm::run::VpEvalWasm;
use namada_vp::native_vp::{self, CtxPostStorageRead, CtxPreStorageRead};
use namada_vp_env::Error;
use proptest::proptest;
use proptest::strategy::Strategy;
use crate::storage_key::{
is_masp_key, is_masp_token_map_key, is_masp_transfer_key,
};
type CA = WasmCacheRwAccess;
type Eval<S> = VpEvalWasm<<S as StateRead>::D, <S as StateRead>::H, CA>;
type Ctx<'ctx, S> = native_vp::Ctx<'ctx, S, VpCache<CA>, Eval<S>>;
type MaspVp<'ctx, S> = super::MaspVp<
'ctx,
Ctx<'ctx, S>,
namada_parameters::Store<
CtxPreStorageRead<'ctx, 'ctx, S, VpCache<CA>, Eval<S>>,
>,
namada_governance::Store<
CtxPreStorageRead<'ctx, 'ctx, S, VpCache<CA>, Eval<S>>,
>,
namada_ibc::Store<
CtxPostStorageRead<'ctx, 'ctx, S, VpCache<CA>, Eval<S>>,
>,
namada_trans_token::Store<
CtxPreStorageRead<'ctx, 'ctx, S, VpCache<CA>, Eval<S>>,
>,
(),
>;
#[test]
fn test_balance_change() {
let mut state = TestState::default();
namada_parameters::init_test_storage(&mut state).unwrap();
let src_key = balance_key(&nam(), &MASP);
let amount = Amount::native_whole(100);
let keys_changed = BTreeSet::from([src_key.clone()]);
let verifiers = Default::default();
state
.db_write(&src_key, Amount::native_whole(100).serialize_to_vec())
.unwrap();
state.db_write(&src_key, amount.serialize_to_vec()).unwrap();
let tx_index = TxIndex::default();
let mut tx = Tx::from_type(namada_tx::data::TxType::Raw);
tx.push_default_inner_tx();
let BatchedTx { tx, cmt } = tx.batch_first_tx();
for new_amount in [150, 1] {
let new_amount = Amount::native_whole(new_amount);
let _ = state
.write_log_mut()
.write(&src_key, new_amount.serialize_to_vec())
.unwrap();
let gas_meter =
RefCell::new(VpGasMeter::new_from_tx_meter(&TxGasMeter::new(
u64::MAX,
namada_parameters::get_gas_scale(&state).unwrap(),
)));
let (vp_vp_cache, _vp_cache_dir) = vp_cache();
let ctx = Ctx::new(
&MASP,
&state,
&tx,
&cmt,
&tx_index,
&gas_meter,
&keys_changed,
&verifiers,
vp_vp_cache,
);
assert!(
MaspVp::validate_tx(
&ctx,
&tx.batch_ref_tx(&cmt),
&keys_changed,
&verifiers
)
.is_err()
);
}
}
proptest! {
#[test]
fn test_no_masp_op_accepted(src_key in arb_key().prop_filter("MASP key", |key| !is_masp_key(key))) {
let mut state = TestState::default();
namada_parameters::init_test_storage(&mut state).unwrap();
let keys_changed = BTreeSet::from([src_key.clone()]);
let verifiers = Default::default();
let tx_index = TxIndex::default();
let mut tx = Tx::from_type(namada_tx::data::TxType::Raw);
tx.push_default_inner_tx();
let BatchedTx { tx, cmt } = tx.batch_first_tx();
let _ = state
.write_log_mut()
.write(&src_key, "test".serialize_to_vec())
.unwrap();
let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter(
&TxGasMeter::new(u64::MAX, namada_parameters::get_gas_scale(&state).unwrap()),
));
let (vp_vp_cache, _vp_cache_dir) = vp_cache();
let ctx = Ctx::new(
&MASP,
&state,
&tx,
&cmt,
&tx_index,
&gas_meter,
&keys_changed,
&verifiers,
vp_vp_cache,
);
assert!(MaspVp::validate_tx(
&ctx,
&tx.batch_ref_tx(&cmt),
&keys_changed,
&verifiers
)
.is_ok());
}
#[test]
fn test_unallowed_masp_keys_rejected(
random_masp_key in arb_account_storage_key(MASP).prop_filter(
"MASP valid key",
|key| !(is_masp_transfer_key(key) || is_masp_token_map_key(key)
))
) {
let mut state = TestState::default();
namada_parameters::init_test_storage(&mut state).unwrap();
let verifiers = Default::default();
let tx_index = TxIndex::default();
let mut tx = Tx::from_type(namada_tx::data::TxType::Raw);
tx.push_default_inner_tx();
let BatchedTx { tx, cmt } = tx.batch_first_tx();
let _ = state
.write_log_mut()
.write(&random_masp_key, "random_value".serialize_to_vec())
.unwrap();
let keys_changed = BTreeSet::from([random_masp_key.clone()]);
let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter(
&TxGasMeter::new(u64::MAX, namada_parameters::get_gas_scale(&state).unwrap()),
));
let (vp_vp_cache, _vp_cache_dir) = vp_cache();
let ctx = Ctx::new(
&MASP,
&state,
&tx,
&cmt,
&tx_index,
&gas_meter,
&keys_changed,
&verifiers,
vp_vp_cache,
);
assert!(matches!(
MaspVp::validate_tx(
&ctx,
&tx.batch_ref_tx(&cmt),
&keys_changed,
&verifiers
),
Err(Error::SimpleMessage(
"A governance proposal is required to modify MASP \
non-transfer keys"
))
));
}
}
}