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::{checked, CheckedAdd, CheckedSub};
use namada_core::booleans::BoolResultUnitExt;
use namada_core::collections::HashSet;
use namada_core::masp::{addr_taddr, encode_asset_type, MaspEpoch, TAddrData};
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_token_map_key,
is_masp_transfer_key, masp_commitment_anchor_key, masp_commitment_tree_key,
masp_convert_anchor_key, masp_nullifier_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(Default, Debug, Clone)]
struct ChangedBalances {
tokens: BTreeMap<AssetType, (Address, token::Denomination, MaspDigitPos)>,
decoder: BTreeMap<TransparentAddress, TAddrData>,
pre: BTreeMap<TransparentAddress, ValueSum<Address, Amount>>,
post: BTreeMap<TransparentAddress, ValueSum<Address, Amount>>,
}
impl<'view, 'ctx: 'view, 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<()> {
let masp_keys_changed: Vec<&Key> =
keys_changed.iter().filter(|key| is_masp_key(key)).collect();
let non_allowed_changes = masp_keys_changed.iter().any(|key| {
!is_masp_transfer_key(key) && !is_masp_token_map_key(key)
});
if non_allowed_changes {
return Err(Error::new_const(
"Found modifications to non-allowed masp keys",
));
}
let masp_token_map_changed = masp_keys_changed
.iter()
.any(|key| is_masp_token_map_key(key));
let masp_transfer_changes = masp_keys_changed
.iter()
.any(|key| is_masp_transfer_key(key));
if masp_token_map_changed && masp_transfer_changes {
Err(Error::new_const(
"Cannot simultaneously do governance proposal and MASP \
transfer",
))
} else if masp_token_map_changed {
Self::is_valid_parameter_change(ctx, tx_data)
} else if masp_transfer_changes {
Self::is_valid_masp_transfer(ctx, tx_data, keys_changed, verifiers)
} else {
Ok(())
}
}
pub fn is_valid_parameter_change(
ctx: &'ctx CTX,
tx: &BatchedTxRef<'_>,
) -> Result<()> {
tx.tx.data(tx.cmt).map_or_else(
|| {
Err(Error::new_const(
"MASP parameter changes require tx data to be present",
))
},
|data| {
Gov::is_proposal_accepted(&ctx.pre(), data.as_ref())?
.ok_or_else(|| {
Error::new_const(
"MASP parameter changes can only be performed by \
a governance proposal that has been accepted",
)
})
},
)
}
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 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",
)?;
unepoched_tokens(token, denom, &mut result.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 mut changed_balances = counterparts_balances
.try_fold(ChangedBalances::default(), |acc, account| {
Self::apply_balance_change(ctx, acc, account)
})?;
let ibc_addr = TAddrData::Addr(address::IBC);
changed_balances
.decoder
.insert(addr_taddr(address::IBC), ibc_addr);
let ChangedBalances {
tokens,
decoder,
pre,
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 {
tokens,
decoder,
pre,
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),
&shielded_tx.sapling_value_balance(),
masp_epoch,
&changed_balances.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 unepoched_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.tokens.contains_key(&vin.asset_type) => {
let (token, denom, digit) =
&changed_balances.tokens[&vin.asset_type];
let epoched_asset_type =
encode_asset_type(token.clone(), *denom, *digit, Some(epoch))
.wrap_err("unable to create asset type")?;
if conversion_state.assets.contains_key(&epoched_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.tokens.contains_key(&out.asset_type) => {
let (token, _denom, digit) =
&changed_balances.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"))
}
fn verify_sapling_balancing_value(
pre: &ValueSum<Address, Amount>,
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());
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())?;
}
_ => {
let error = Error::new_const("Unable to decode asset type");
tracing::debug!("{error}");
return Err(error);
}
}
}
if acc == ValueSum::from_sum(pre.clone()) {
Ok(())
} else {
let error = Error::new_const(
"MASP balance change not equal to Sapling value balance",
);
tracing::debug!("{error}");
Err(error)
}
}
#[cfg(test)]
mod shielded_token_tests {
use std::cell::RefCell;
use std::collections::BTreeSet;
use namada_core::address::testing::nam;
use namada_core::address::MASP;
use namada_core::borsh::BorshSerializeExt;
use namada_core::masp::TokenMap;
use namada_gas::{TxGasMeter, VpGasMeter};
use namada_state::testing::{arb_account_storage_key, arb_key, TestState};
use namada_state::{StateRead, TxIndex};
use namada_trans_token::storage_key::balance_key;
use namada_trans_token::Amount;
use namada_tx::{BatchedTx, Tx};
use namada_vm::wasm::compilation_cache::common::testing::vp_cache;
use namada_vm::wasm::run::VpEvalWasm;
use namada_vm::wasm::VpCache;
use namada_vm::WasmCacheRwAccess;
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,
masp_token_map_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()
);
}
}
#[test]
fn test_mixed_keys_rejected() {
let mut state = TestState::default();
namada_parameters::init_test_storage(&mut state).unwrap();
let balance_key = balance_key(&nam(), &MASP);
let token_map_key = masp_token_map_key();
let keys_changed =
BTreeSet::from([balance_key.clone(), token_map_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 amount = Amount::native_whole(100);
let _ = state
.write_log_mut()
.write(&balance_key, amount.serialize_to_vec())
.unwrap();
let token_map = TokenMap::new();
let _ = state
.write_log_mut()
.write(&token_map_key, token_map.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!(matches!(
MaspVp::validate_tx(
&ctx,
&tx.batch_ref_tx(&cmt),
&keys_changed,
&verifiers
),
Err(Error::SimpleMessage(
"Cannot simultaneously do governance proposal and MASP \
transfer"
))
));
}
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(
"Found modifications to non-allowed masp keys"
))
));
}
}
}