From 75076bde6cee3f87b1e9798cd400641e533e5c0c Mon Sep 17 00:00:00 2001 From: Murisi Tarusenga Date: Mon, 22 Jan 2024 14:41:50 +0200 Subject: [PATCH] Hashed the token denomination into AssetTypes to tighten HW wallet signing. --- apps/src/lib/client/rpc.rs | 11 +- core/src/ledger/masp_conversions.rs | 87 ++++++++++----- core/src/proto/types.rs | 4 +- core/src/types/masp.rs | 7 +- sdk/src/lib.rs | 14 ++- sdk/src/masp.rs | 167 ++++++++++++++++------------ sdk/src/queries/shell.rs | 19 +++- sdk/src/rpc.rs | 13 +++ sdk/src/signing.rs | 32 ++++-- sdk/src/tx.rs | 33 ++++-- shared/src/ledger/native_vp/masp.rs | 63 +++++++---- 11 files changed, 300 insertions(+), 150 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 03d5af938e0..6db4ba624af 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2355,7 +2355,7 @@ pub async fn query_conversions( .expect("Conversions should be defined"); // Track whether any non-sentinel conversions are found let mut conversions_found = false; - for (addr, epoch, amt) in conversions.values() { + for (addr, _denom, digit, epoch, amt) in conversions.values() { // If the user has specified any targets, then meet them // If we have a sentinel conversion, then skip printing if matches!(&target_token, Some(target) if target != addr) @@ -2368,8 +2368,9 @@ pub async fn query_conversions( // Print the asset to which the conversion applies display!( context.io(), - "{}[{}]: ", + "{}*2^{}[{}]: ", tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + *digit as u8 * 64, epoch, ); // Now print out the components of the allowed conversion @@ -2377,14 +2378,15 @@ pub async fn query_conversions( for (asset_type, val) in amt.components() { // Look up the address and epoch of asset to facilitate pretty // printing - let (addr, epoch, _) = &conversions[asset_type]; + let (addr, _denom, digit, epoch, _) = &conversions[asset_type]; // Now print out this component of the conversion display!( context.io(), - "{}{} {}[{}]", + "{}{} {}*2^{}[{}]", prefix, val, tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + *digit as u8 * 64, epoch ); // Future iterations need to be prefixed with + @@ -2407,6 +2409,7 @@ pub async fn query_conversion( asset_type: AssetType, ) -> Option<( Address, + token::Denomination, MaspDenom, Epoch, masp_primitives::transaction::components::I128Sum, diff --git a/core/src/ledger/masp_conversions.rs b/core/src/ledger/masp_conversions.rs index b1ee74afb1d..3a96ae31467 100644 --- a/core/src/ledger/masp_conversions.rs +++ b/core/src/ledger/masp_conversions.rs @@ -16,7 +16,7 @@ use crate::ledger::storage_api::{StorageRead, StorageWrite}; use crate::types::address::{Address, MASP}; use crate::types::dec::Dec; use crate::types::storage::Epoch; -use crate::types::token::{self, DenominatedAmount, MaspDenom}; +use crate::types::token::{self, DenominatedAmount, Denomination, MaspDenom}; use crate::types::uint::Uint; /// A representation of the conversion state @@ -32,7 +32,12 @@ pub struct ConversionState { #[allow(clippy::type_complexity)] pub assets: BTreeMap< AssetType, - ((Address, MaspDenom), Epoch, AllowedConversion, usize), + ( + (Address, Denomination, MaspDenom), + Epoch, + AllowedConversion, + usize, + ), >, } @@ -41,7 +46,7 @@ pub struct ConversionState { pub fn calculate_masp_rewards( wl_storage: &mut WlStorage, addr: &Address, -) -> crate::ledger::storage_api::Result<(u128, u128)> +) -> crate::ledger::storage_api::Result<((u128, u128), Denomination)> where D: 'static + DB + for<'iter> DBIter<'iter>, H: 'static + StorageHasher, @@ -188,7 +193,7 @@ where wl_storage.write(&token::masp_last_locked_ratio_key(addr), locked_ratio)?; - Ok((noterized_inflation, precision)) + Ok(((noterized_inflation, precision), denomination)) } // This is only enabled when "wasm-runtime" is on, because we're using rayon @@ -214,7 +219,9 @@ where use crate::ledger::storage_api::ResultExt; use crate::types::masp::encode_asset_type; use crate::types::storage::{Key, KeySeg}; - use crate::types::token::MASP_CONVERT_ANCHOR_KEY; + use crate::types::token::{ + MASP_CONVERT_ANCHOR_KEY, NATIVE_MAX_DECIMAL_PLACES, + }; // The derived conversions will be placed in MASP address space let masp_addr = MASP; @@ -226,6 +233,7 @@ where .values() .cloned() .collect(); + let mut masp_reward_denoms = BTreeMap::new(); // Put the native rewards first because other inflation computations depend // on it let native_token = wl_storage.storage.native_token.clone(); @@ -246,23 +254,45 @@ where // notes clients have to use. This trick works under the assumption that // reward tokens will then be reinflated back to the current epoch. let reward_assets = [ - encode_asset_type(Some(Epoch(0)), &native_token, MaspDenom::Zero) - .into_storage_result()?, - encode_asset_type(Some(Epoch(0)), &native_token, MaspDenom::One) - .into_storage_result()?, - encode_asset_type(Some(Epoch(0)), &native_token, MaspDenom::Two) - .into_storage_result()?, - encode_asset_type(Some(Epoch(0)), &native_token, MaspDenom::Three) - .into_storage_result()?, + encode_asset_type( + Some(Epoch(0)), + &native_token, + NATIVE_MAX_DECIMAL_PLACES.into(), + MaspDenom::Zero, + ) + .into_storage_result()?, + encode_asset_type( + Some(Epoch(0)), + &native_token, + NATIVE_MAX_DECIMAL_PLACES.into(), + MaspDenom::One, + ) + .into_storage_result()?, + encode_asset_type( + Some(Epoch(0)), + &native_token, + NATIVE_MAX_DECIMAL_PLACES.into(), + MaspDenom::Two, + ) + .into_storage_result()?, + encode_asset_type( + Some(Epoch(0)), + &native_token, + NATIVE_MAX_DECIMAL_PLACES.into(), + MaspDenom::Three, + ) + .into_storage_result()?, ]; // Conversions from the previous to current asset for each address let mut current_convs = - BTreeMap::<(Address, MaspDenom), AllowedConversion>::new(); + BTreeMap::<(Address, Denomination, MaspDenom), AllowedConversion>::new( + ); // Native token inflation values are always with respect to this let mut ref_inflation = 0; // Reward all tokens according to above reward rates for addr in &masp_reward_keys { - let reward = calculate_masp_rewards(wl_storage, addr)?; + let (reward, denom) = calculate_masp_rewards(wl_storage, addr)?; + masp_reward_denoms.insert(addr, denom); if *addr == native_token { // The reference inflation is the denominator of the native token // inflation, which is always a constant @@ -272,7 +302,7 @@ where let addr_bal: token::Amount = wl_storage .read(&token::balance_key(addr, &masp_addr))? .unwrap_or_default(); - for denom in token::MaspDenom::iter() { + for digit in token::MaspDenom::iter() { // Provide an allowed conversion from previous timestamp. The // negative sign allows each instance of the old asset to be // cancelled out/replaced with the new asset @@ -280,12 +310,14 @@ where Some(wl_storage.storage.last_epoch), addr, denom, + digit, ) .into_storage_result()?; let new_asset = encode_asset_type( Some(wl_storage.storage.block.epoch), addr, denom, + digit, ) .into_storage_result()?; // Get the last rewarded amount of the native token @@ -318,7 +350,7 @@ where // intermediate native tokens cancel/ // telescope out current_convs.insert( - (addr.clone(), denom), + (addr.clone(), denom, digit), (MaspAmount::from_pair( old_asset, -(*normed_inflation as i128), @@ -332,7 +364,7 @@ where .into(), ); // Operations that happen exactly once for each token - if denom == MaspDenom::Three { + if digit == MaspDenom::Three { // The reward for each reward.1 units of the current asset // is reward.0 units of the reward token let native_reward = @@ -367,20 +399,20 @@ where // conversions are added together, the // intermediate tokens cancel/ telescope out current_convs.insert( - (addr.clone(), denom), + (addr.clone(), denom, digit), (MaspAmount::from_pair(old_asset, -(reward.1 as i128)) .unwrap() + MaspAmount::from_pair(new_asset, reward.1 as i128) .unwrap() + MaspAmount::from_pair( - reward_assets[denom as usize], + reward_assets[digit as usize], real_reward as i128, ) .unwrap()) .into(), ); // Operations that happen exactly once for each token - if denom == MaspDenom::Three { + if digit == MaspDenom::Three { // The reward for each reward.1 units of the current asset // is reward.0 units of the reward token total_reward += (addr_bal * (reward.0, reward.1)).0; @@ -390,7 +422,7 @@ where wl_storage.storage.conversion_state.assets.insert( old_asset, ( - (addr.clone(), denom), + (addr.clone(), denom, digit), wl_storage.storage.last_epoch, MaspAmount::zero().into(), 0, @@ -470,20 +502,21 @@ where // Add purely decoding entries to the assets map. These will be // overwritten before the creation of the next commitment tree - for addr in masp_reward_keys { - for denom in token::MaspDenom::iter() { + for (addr, denom) in masp_reward_denoms { + for digit in token::MaspDenom::iter() { // Add the decoding entry for the new asset type. An uncommitted // node position is used since this is not a conversion. let new_asset = encode_asset_type( Some(wl_storage.storage.block.epoch), - &addr, + addr, denom, + digit, ) .into_storage_result()?; wl_storage.storage.conversion_state.assets.insert( new_asset, ( - (addr.clone(), denom), + (addr.clone(), denom, digit), wl_storage.storage.block.epoch, MaspAmount::zero().into(), wl_storage.storage.conversion_state.tree.size(), @@ -604,7 +637,7 @@ mod tests { } } - pub fn tokens() -> HashMap { + pub fn tokens() -> HashMap { vec![ (address::nam(), ("nam", 6.into())), (address::btc(), ("btc", 8.into())), diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index a972b846a0f..0eac2c540aa 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -29,6 +29,7 @@ use crate::types::keccak::{keccak_hash, KeccakHash}; use crate::types::key::{self, *}; use crate::types::storage::Epoch; use crate::types::time::DateTimeUtc; +use crate::types::token; use crate::types::token::MaspDenom; use crate::types::transaction::protocol::ProtocolTx; use crate::types::transaction::{ @@ -767,7 +768,8 @@ pub struct MaspBuilder { pub target: crate::types::hash::Hash, /// The decoded set of asset types used by the transaction. Useful for /// offline wallets trying to display AssetTypes. - pub asset_types: HashSet<(Address, MaspDenom, Option)>, + pub asset_types: + HashSet<(Address, token::Denomination, MaspDenom, Option)>, /// Track how Info objects map to descriptors and outputs #[serde( serialize_with = "borsh_serde::", diff --git a/core/src/types/masp.rs b/core/src/types/masp.rs index 9290ad800e7..7bbbac57a36 100644 --- a/core/src/types/masp.rs +++ b/core/src/types/masp.rs @@ -15,16 +15,17 @@ use crate::types::string_encoding::{ self, MASP_EXT_FULL_VIEWING_KEY_HRP, MASP_EXT_SPENDING_KEY_HRP, MASP_PAYMENT_ADDRESS_HRP, }; -use crate::types::token::MaspDenom; +use crate::types::token::{Denomination, MaspDenom}; /// Make asset type corresponding to given address and epoch pub fn encode_asset_type( epoch: Option, token: &Address, - denom: MaspDenom, + denom: Denomination, + digit: MaspDenom, ) -> Result { // Timestamp the chosen token with the current epoch - let token_bytes = (token, denom, epoch).serialize_to_vec(); + let token_bytes = (token, denom, digit, epoch).serialize_to_vec(); // Generate the unique asset identifier from the unique token address AssetType::new(token_bytes.as_ref()).map_err(|_| { std::io::Error::new( diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 035c2ff3e2d..6f8da3a8572 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -1033,15 +1033,21 @@ pub mod testing { MaspTxType::Shielding => { transfer.target = MASP; // Set the transparent amount and token - let ((addr, denom, _epoch), value) = asset_types.iter().next().unwrap(); - transfer.amount = DenominatedAmount::native(token::Amount::from_masp_denominated(*value, *denom)); + let ((addr, denom, digit, _epoch), value) = asset_types.iter().next().unwrap(); + transfer.amount = DenominatedAmount::new( + token::Amount::from_masp_denominated(*value, *digit), + *denom, + ); transfer.token = addr.clone(); }, MaspTxType::Deshielding => { transfer.source = MASP; // Set the transparent amount and token - let ((addr, denom, _epoch), value) = asset_types.iter().next().unwrap(); - transfer.amount = DenominatedAmount::native(token::Amount::from_masp_denominated(*value, *denom)); + let ((addr, denom, digit, _epoch), value) = asset_types.iter().next().unwrap(); + transfer.amount = DenominatedAmount::new( + token::Amount::from_masp_denominated(*value, *digit), + *denom, + ); transfer.token = addr.clone(); }, } diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 24a2eb4ad35..b2f20dcd2bf 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -59,7 +59,8 @@ use namada_core::types::storage::{BlockHeight, Epoch, Key, KeySeg, TxIndex}; use namada_core::types::time::{DateTimeUtc, DurationSecs}; use namada_core::types::token; use namada_core::types::token::{ - MaspDenom, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, + Denomination, MaspDenom, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, + TX_KEY_PREFIX, }; use namada_core::types::transaction::WrapperTx; use rand_core::{CryptoRng, OsRng, RngCore}; @@ -73,7 +74,7 @@ use crate::error::{Error, PinnedBalanceError, QueryError}; use crate::io::Io; use crate::proto::Tx; use crate::queries::Client; -use crate::rpc::{query_conversion, query_storage_value}; +use crate::rpc::{query_conversion, query_denom, query_storage_value}; use crate::tendermint_rpc::query::Query; use crate::tendermint_rpc::Order; use crate::{display_line, edisplay_line, rpc, MaybeSend, MaybeSync, Namada}; @@ -532,7 +533,10 @@ pub struct ShieldedContext { /// The set of note positions that have been spent pub spents: HashSet, /// Maps asset types to their decodings - pub asset_types: HashMap)>, + pub asset_types: HashMap< + AssetType, + (Address, token::Denomination, MaspDenom, Option), + >, /// Maps note positions to their corresponding viewing keys pub vk_map: HashMap, } @@ -905,22 +909,23 @@ impl ShieldedContext { &mut self, client: &C, asset_type: AssetType, - ) -> Option<(Address, MaspDenom, Option)> { + ) -> Option<(Address, Denomination, MaspDenom, Option)> { // Try to find the decoding in the cache if let decoded @ Some(_) = self.asset_types.get(&asset_type) { return decoded.cloned(); } // Query for the ID of the last accepted transaction - let (addr, denom, ep, _conv, _path): ( + let (addr, denom, digit, ep, _conv, _path): ( Address, + Denomination, MaspDenom, _, I128Sum, MerklePath, ) = rpc::query_conversion(client, asset_type).await?; self.asset_types - .insert(asset_type, (addr.clone(), denom, Some(ep))); - Some((addr, denom, Some(ep))) + .insert(asset_type, (addr.clone(), denom, digit, Some(ep))); + Some((addr, denom, digit, Some(ep))) } /// Query the ledger for the conversion that is allowed for the given asset @@ -935,14 +940,13 @@ impl ShieldedContext { conversions.entry(asset_type) { // Query for the ID of the last accepted transaction - if let Some((addr, denom, ep, conv, path)) = - query_conversion(client, asset_type).await - { - self.asset_types.insert(asset_type, (addr, denom, Some(ep))); - // If the conversion is 0, then we just have a pure decoding - if !conv.is_zero() { - conv_entry.insert((conv.into(), path, 0)); - } + let Some((addr, denom, digit, ep, conv, path)) = + query_conversion(client, asset_type).await else { return }; + self.asset_types + .insert(asset_type, (addr, denom, digit, Some(ep))); + // If the conversion is 0, then we just have a pure decoding + if !conv.is_zero() { + conv_entry.insert((conv.into(), path, 0)); } } } @@ -1056,21 +1060,22 @@ impl ShieldedContext { let (target_asset_type, forward_conversion) = self .decode_asset_type(client, asset_type) .await - .map(|(addr, denom, epoch)| { - encode_asset_type(epoch.map(|_| target_epoch), &addr, denom) - .map(|asset_type| { - ( - asset_type, - epoch.map_or(false, |epoch| { - target_epoch >= epoch - }), - ) - }) - .map_err(|_| { - Error::Other( - "unable to create asset type".to_string(), - ) - }) + .map(|(addr, denom, digit, epoch)| { + encode_asset_type( + epoch.map(|_| target_epoch), + &addr, + denom, + digit, + ) + .map(|asset_type| { + ( + asset_type, + epoch.map_or(false, |epoch| target_epoch >= epoch), + ) + }) + .map_err(|_| { + Error::Other("unable to create asset type".to_string()) + }) }) .transpose()? .unwrap_or((asset_type, false)); @@ -1394,11 +1399,11 @@ impl ShieldedContext { let decoded = self.decode_asset_type(client, *asset_type).await; // Only assets with the target timestamp count match decoded { - Some((address, denom, epoch)) + Some((address, _denom, digit, epoch)) if epoch.map_or(true, |epoch| epoch <= target_epoch) => { let decoded_change = - token::Change::from_masp_denominated(*val, denom) + token::Change::from_masp_denominated(*val, digit) .expect("expected this to fit"); res += ValueSum::from_pair(address, decoded_change) .expect("expected this to fit"); @@ -1419,11 +1424,11 @@ impl ShieldedContext { let mut res = MaspAmount::zero(); for (asset_type, val) in amt.components() { // Decode the asset type - if let Some((addr, denom, epoch)) = + if let Some((addr, _denom, digit, epoch)) = self.decode_asset_type(client, *asset_type).await { let decoded_change = - token::Change::from_masp_denominated(*val, denom) + token::Change::from_masp_denominated(*val, digit) .expect("expected this to fit"); res += MaspAmount::from_pair((epoch, addr), decoded_change) .expect("unable to construct decoded amount"); @@ -1438,15 +1443,18 @@ impl ShieldedContext { &mut self, client: &C, amt: I128Sum, - ) -> ValueSum<(AssetType, Address, MaspDenom, Option), i128> { + ) -> ValueSum< + (AssetType, Address, Denomination, MaspDenom, Option), + i128, + > { let mut res = ValueSum::zero(); for (asset_type, val) in amt.components() { // Decode the asset type - if let Some((addr, denom, epoch)) = + if let Some((addr, denom, digit, epoch)) = self.decode_asset_type(client, *asset_type).await { res += ValueSum::from_pair( - (*asset_type, addr, denom, epoch), + (*asset_type, addr, denom, digit, epoch), *val, ) .expect("unable to construct decoded amount"); @@ -1571,10 +1579,21 @@ impl ShieldedContext { ); // Convert transaction amount into MASP types + let Some(denom) = query_denom(context.client(), token).await else { + return Err(TransferErr::General(Error::from(QueryError::General(format!( + "denomination for token {token}" + ))))) + }; let (asset_types, masp_amount) = context .shielded_mut() .await - .convert_amount(context.client(), epoch, token, amount.amount()) + .convert_amount( + context.client(), + epoch, + token, + denom, + amount.amount(), + ) .await?; // If there are shielded inputs @@ -1625,9 +1644,9 @@ impl ShieldedContext { source_enc.as_ref(), )); let script = TransparentAddress(hash.into()); - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + for (digit, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { - let amount_part = denom.denominate(&amount.amount()); + let amount_part = digit.denominate(&amount.amount()); // Skip adding an input if its value is 0 if amount_part != 0 { builder @@ -1680,13 +1699,16 @@ impl ShieldedContext { // Now handle the outputs of this transaction // Loop through the value balance components and see which // ones can be given to the receiver - for ((asset_type, vbal_token, vbal_denom, vbal_epoch), val) in - value_balance.components() + for ( + (asset_type, vbal_token, vbal_denom, vbal_digit, vbal_epoch), + val, + ) in value_balance.components() { - let rem_amount = &mut rem_amount[*vbal_denom as usize]; + let rem_amount = &mut rem_amount[*vbal_digit as usize]; // Only asset types with the correct token can contribute. But // there must be a demonstrated need for it. if vbal_token == token + && *vbal_denom == denom && vbal_epoch.map_or(true, |vbal_epoch| vbal_epoch <= epoch) && *rem_amount > 0 { @@ -2040,20 +2062,22 @@ impl ShieldedContext { client: &C, epoch: Epoch, token: Address, - denom: MaspDenom, + denom: Denomination, + digit: MaspDenom, ) -> Result { - let mut asset_type = encode_asset_type(Some(epoch), &token, denom) - .map_err(|_| { - Error::Other("unable to create asset type".to_string()) - })?; + let mut asset_type = + encode_asset_type(Some(epoch), &token, denom, digit).map_err( + |_| Error::Other("unable to create asset type".to_string()), + )?; if self.decode_asset_type(client, asset_type).await.is_none() { // If we fail to decode the epoched asset type, then remove the // epoch - asset_type = - encode_asset_type(None, &token, denom).map_err(|_| { + asset_type = encode_asset_type(None, &token, denom, digit) + .map_err(|_| { Error::Other("unable to create asset type".to_string()) })?; - self.asset_types.insert(asset_type, (token, denom, None)); + self.asset_types + .insert(asset_type, (token, denom, digit, None)); } Ok(asset_type) } @@ -2064,17 +2088,18 @@ impl ShieldedContext { client: &C, epoch: Epoch, token: &Address, + denom: Denomination, val: token::Amount, ) -> Result<([AssetType; 4], U64Sum), Error> { let mut amount = U64Sum::zero(); let mut asset_types = Vec::new(); - for denom in MaspDenom::iter() { + for digit in MaspDenom::iter() { let asset_type = self - .get_asset_type(client, epoch, token.clone(), denom) + .get_asset_type(client, epoch, token.clone(), denom, digit) .await?; // Combine the value and unit into one amount amount += - U64Sum::from_nonnegative(asset_type, denom.denominate(&val)) + U64Sum::from_nonnegative(asset_type, digit.denominate(&val)) .map_err(|_| { Error::Other("invalid value for amount".to_string()) })?; @@ -2245,6 +2270,7 @@ pub mod testing { use crate::masp_primitives::sapling::keys::OutgoingViewingKey; use crate::masp_primitives::sapling::redjubjub::PrivateKey; use crate::masp_primitives::transaction::components::transparent::testing::arb_transparent_address; + use crate::token::testing::arb_denomination; #[derive(Debug, Clone)] // Adapts a CSPRNG from a PRNG for proptesting @@ -2628,16 +2654,17 @@ pub mod testing { // Generate arbitrary spend descriptions with the given asset type // partitioning the given values pub fn arb_spend_descriptions( - asset: (Address, MaspDenom, Option), + asset: (Address, Denomination, MaspDenom, Option), values: Vec, )(partition in arb_partition(values))( spend_description in partition .iter() .map(|value| arb_spend_description( encode_asset_type( - asset.2, + asset.3, &asset.0, asset.1, + asset.2, ).unwrap(), *value, )).collect::>() @@ -2650,16 +2677,17 @@ pub mod testing { // Generate arbitrary output descriptions with the given asset type // partitioning the given values pub fn arb_output_descriptions( - asset: (Address, MaspDenom, Option), + asset: (Address, Denomination, MaspDenom, Option), values: Vec, )(partition in arb_partition(values))( output_description in partition .iter() .map(|value| arb_output_description( encode_asset_type( - asset.2, + asset.3, &asset.0, asset.1, + asset.2, ).unwrap(), *value, )).collect::>() @@ -2672,7 +2700,7 @@ pub mod testing { // Generate arbitrary spend descriptions with the given asset type // partitioning the given values pub fn arb_txouts( - asset: (Address, MaspDenom, Option), + asset: (Address, Denomination, MaspDenom, Option), values: Vec, address: TransparentAddress, )( @@ -2682,9 +2710,10 @@ pub mod testing { .iter() .map(|value| TxOut { asset_type: encode_asset_type( - asset.2, + asset.3, &asset.0, asset.1, + asset.2, ).unwrap(), value: *value, address, @@ -2696,7 +2725,7 @@ pub mod testing { // Generate an arbitrary shielded MASP transaction builder pub fn arb_shielded_builder(asset_range: impl Into)( assets in collection::hash_map( - (arb_address(), arb_masp_denom(), option::of(arb_epoch())), + (arb_address(), arb_denomination(), arb_masp_denom(), option::of(arb_epoch())), collection::vec(..MAX_MONEY, ..MAX_SPLITS), asset_range, ), @@ -2714,7 +2743,7 @@ pub mod testing { assets in Just(assets), ) -> ( Builder::>, - HashMap<(Address, MaspDenom, Option), u64>, + HashMap<(Address, Denomination, MaspDenom, Option), u64>, ) { let mut builder = Builder::::new_with_rng( NETWORK, @@ -2748,7 +2777,7 @@ pub mod testing { asset_range: impl Into, )( assets in collection::hash_map( - (arb_address(), arb_masp_denom(), option::of(arb_epoch())), + (arb_address(), arb_denomination(), arb_masp_denom(), option::of(arb_epoch())), collection::vec(..MAX_MONEY, ..MAX_SPLITS), asset_range, ), @@ -2766,7 +2795,7 @@ pub mod testing { assets in Just(assets), ) -> ( Builder::>, - HashMap<(Address, MaspDenom, Option), u64>, + HashMap<(Address, Denomination, MaspDenom, Option), u64>, ) { let mut builder = Builder::::new_with_rng( NETWORK, @@ -2793,7 +2822,7 @@ pub mod testing { asset_range: impl Into, )( assets in collection::hash_map( - (arb_address(), arb_masp_denom(), option::of(arb_epoch())), + (arb_address(), arb_denomination(), arb_masp_denom(), option::of(arb_epoch())), collection::vec(..MAX_MONEY, ..MAX_SPLITS), asset_range, ), @@ -2811,7 +2840,7 @@ pub mod testing { assets in Just(assets), ) -> ( Builder::>, - HashMap<(Address, MaspDenom, Option), u64>, + HashMap<(Address, Denomination, MaspDenom, Option), u64>, ) { let mut builder = Builder::::new_with_rng( NETWORK, @@ -2846,7 +2875,7 @@ pub mod testing { (builder, asset_types) in arb_shielded_builder(asset_range), epoch in arb_epoch(), rng in arb_rng().prop_map(TestCsprng), - ) -> (ShieldedTransfer, HashMap<(Address, MaspDenom, Option), u64>) { + ) -> (ShieldedTransfer, HashMap<(Address, Denomination, MaspDenom, Option), u64>) { let (masp_tx, metadata) = builder.clone().build( &MockTxProver(Mutex::new(rng)), &FeeRule::non_standard(U64Sum::zero()), @@ -2872,7 +2901,7 @@ pub mod testing { ), epoch in arb_epoch(), rng in arb_rng().prop_map(TestCsprng), - ) -> (ShieldedTransfer, HashMap<(Address, MaspDenom, Option), u64>) { + ) -> (ShieldedTransfer, HashMap<(Address, Denomination, MaspDenom, Option), u64>) { let (masp_tx, metadata) = builder.clone().build( &MockTxProver(Mutex::new(rng)), &FeeRule::non_standard(U64Sum::zero()), @@ -2898,7 +2927,7 @@ pub mod testing { ), epoch in arb_epoch(), rng in arb_rng().prop_map(TestCsprng), - ) -> (ShieldedTransfer, HashMap<(Address, MaspDenom, Option), u64>) { + ) -> (ShieldedTransfer, HashMap<(Address, Denomination, MaspDenom, Option), u64>) { let (masp_tx, metadata) = builder.clone().build( &MockTxProver(Mutex::new(rng)), &FeeRule::non_standard(U64Sum::zero()), diff --git a/sdk/src/queries/shell.rs b/sdk/src/queries/shell.rs index aafbf29702c..8a1beda0c6a 100644 --- a/sdk/src/queries/shell.rs +++ b/sdk/src/queries/shell.rs @@ -17,7 +17,7 @@ use namada_core::types::hash::Hash; use namada_core::types::storage::{ self, BlockHeight, BlockResults, Epoch, KeySeg, PrefixValue, }; -use namada_core::types::token::MaspDenom; +use namada_core::types::token::{Denomination, MaspDenom}; #[cfg(any(test, feature = "async-client"))] use namada_core::types::transaction::TxResult; @@ -33,12 +33,15 @@ use crate::tendermint::merkle::proof::ProofOps; type ConversionWithoutPath = ( Address, + Denomination, + MaspDenom, Epoch, masp_primitives::transaction::components::I128Sum, ); type Conversion = ( Address, + Denomination, MaspDenom, Epoch, masp_primitives::transaction::components::I128Sum, @@ -174,9 +177,14 @@ where .conversion_state .assets .iter() - .map(|(&asset_type, ((ref addr, _), epoch, ref conv, _))| { - (asset_type, (addr.clone(), *epoch, conv.clone().into())) - }) + .map( + |(&asset_type, ((ref addr, denom, digit), epoch, ref conv, _))| { + ( + asset_type, + (addr.clone(), *denom, *digit, *epoch, conv.clone().into()), + ) + }, + ) .collect()) } @@ -190,7 +198,7 @@ where H: 'static + StorageHasher + Sync, { // Conversion values are constructed on request - if let Some(((addr, denom), epoch, conv, pos)) = ctx + if let Some(((addr, denom, digit), epoch, conv, pos)) = ctx .wl_storage .storage .conversion_state @@ -200,6 +208,7 @@ where Ok(Some(( addr.clone(), *denom, + *digit, *epoch, Into::::into( conv.clone(), diff --git a/sdk/src/rpc.rs b/sdk/src/rpc.rs index ea6c8439aba..9a69168142a 100644 --- a/sdk/src/rpc.rs +++ b/sdk/src/rpc.rs @@ -255,6 +255,7 @@ pub async fn query_conversion( asset_type: AssetType, ) -> Option<( Address, + Denomination, MaspDenom, Epoch, masp_primitives::transaction::components::I128Sum, @@ -273,6 +274,8 @@ pub async fn query_conversions( AssetType, ( Address, + Denomination, + MaspDenom, Epoch, masp_primitives::transaction::components::I128Sum, ), @@ -1050,6 +1053,16 @@ pub async fn enriched_bonds_and_unbonds( ) } +/// Query the denomination of the given token +pub async fn query_denom( + client: &C, + token: &Address, +) -> Option { + unwrap_client_response::>( + RPC.vp().token().denomination(client, token).await, + ) +} + /// Get the correct representation of the amount given the token type. pub async fn validate_amount( context: &N, diff --git a/sdk/src/signing.rs b/sdk/src/signing.rs index db7b7805496..9b07a4e57ae 100644 --- a/sdk/src/signing.rs +++ b/sdk/src/signing.rs @@ -24,7 +24,9 @@ use namada_core::types::storage::Epoch; use namada_core::types::token; use namada_core::types::token::Transfer; // use namada_core::types::storage::Key; -use namada_core::types::token::{Amount, DenominatedAmount, MaspDenom}; +use namada_core::types::token::{ + Amount, DenominatedAmount, Denomination, MaspDenom, +}; use namada_core::types::transaction::account::{InitAccount, UpdateAccount}; use namada_core::types::transaction::governance::{ InitProposalData, VoteProposalData, @@ -726,17 +728,23 @@ async fn make_ledger_amount_asset( output: &mut Vec, amount: u64, token: &AssetType, - assets: &HashMap)>, + assets: &HashMap< + AssetType, + (Address, Denomination, MaspDenom, Option), + >, prefix: &str, ) { - if let Some((token, denom, _epoch)) = assets.get(token) { + if let Some((token, denom, digit, _epoch)) = assets.get(token) { // If the AssetType can be decoded, then at least display Addressees if let Some(token) = tokens.get(token) { output.push(format!( "{}Amount : {} {}", prefix, token.to_uppercase(), - token::Amount::from_masp_denominated(amount, *denom), + DenominatedAmount::new( + token::Amount::from_masp_denominated(amount, *digit), + *denom + ), )); } else { output.extend(vec![ @@ -744,7 +752,10 @@ async fn make_ledger_amount_asset( format!( "{}Amount : {}", prefix, - token::Amount::from_masp_denominated(amount, *denom) + DenominatedAmount::new( + token::Amount::from_masp_denominated(amount, *digit), + *denom + ), ), ]); } @@ -819,7 +830,10 @@ pub async fn make_ledger_masp_endpoints( output: &mut Vec, transfer: &Transfer, builder: Option<&MaspBuilder>, - assets: &HashMap)>, + assets: &HashMap< + AssetType, + (Address, Denomination, MaspDenom, Option), + >, ) { if transfer.source != MASP { output.push(format!("Sender : {}", transfer.source)); @@ -1269,13 +1283,13 @@ pub async fn to_ledger_vector( Section::MaspBuilder(builder) if builder.target == shielded_hash => { - for (addr, denom, epoch) in &builder.asset_types { - match encode_asset_type(*epoch, addr, *denom) { + for (addr, denom, digit, epoch) in &builder.asset_types { + match encode_asset_type(*epoch, addr, *denom, *digit) { Err(_) => None, Ok(asset) => { asset_types.insert( asset, - (addr.clone(), *denom, *epoch), + (addr.clone(), *denom, *digit, *epoch), ); Some(builder) } diff --git a/sdk/src/tx.rs b/sdk/src/tx.rs index dbc522c8296..eb3cf9a8226 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -60,7 +60,7 @@ use crate::masp::{ShieldedContext, ShieldedTransfer}; use crate::proto::{MaspBuilder, Tx}; use crate::queries::Client; use crate::rpc::{ - self, query_wasm_code_hash, validate_amount, InnerTxResult, + self, query_denom, query_wasm_code_hash, validate_amount, InnerTxResult, TxBroadcastData, TxResponse, }; use crate::signing::{self, SigningTxData, TxSourcePostBalance}; @@ -2215,7 +2215,12 @@ where /// Try to decode the given asset type and add its decoding to the supplied set. /// Returns true only if a new decoding has been added to the given set. async fn add_asset_type( - asset_types: &mut HashSet<(Address, MaspDenom, Option)>, + asset_types: &mut HashSet<( + Address, + token::Denomination, + MaspDenom, + Option, + )>, context: &impl Namada, asset_type: AssetType, ) -> bool { @@ -2237,8 +2242,10 @@ async fn add_asset_type( async fn used_asset_types( context: &impl Namada, builder: &Builder, -) -> std::result::Result)>, RpcError> -{ +) -> std::result::Result< + HashSet<(Address, token::Denomination, MaspDenom, Option)>, + RpcError, +> { let mut asset_types = HashSet::new(); // Collect all the asset types used in the Sapling inputs for input in builder.sapling_inputs() { @@ -2406,7 +2413,7 @@ async fn construct_shielded_parts( ) -> Result< Option<( ShieldedTransfer, - HashSet<(Address, MaspDenom, Option)>, + HashSet<(Address, token::Denomination, MaspDenom, Option)>, )>, > { let stx_result = @@ -2678,12 +2685,24 @@ pub async fn gen_ibc_shielded_transfer( shielded: None, }; if let Some(shielded_transfer) = shielded_transfer { + let denom = + query_denom(context.client(), &token).await.ok_or_else(|| { + Error::Other( + "unable to determine token denomination".to_string(), + ) + })?; // TODO: Workaround for decoding the asset_type later let mut shielded = context.shielded_mut().await; - for denom in MaspDenom::iter() { + for digit in MaspDenom::iter() { let epoch = shielded_transfer.epoch; shielded - .get_asset_type(context.client(), epoch, token.clone(), denom) + .get_asset_type( + context.client(), + epoch, + token.clone(), + denom, + digit, + ) .await .map_err(|_| { Error::Other("unable to create asset type".to_string()) diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index 6b4bed6852a..501f0703bf5 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -11,6 +11,7 @@ use masp_primitives::transaction::components::I128Sum; use masp_primitives::transaction::Transaction; use namada_core::ledger::gas::MASP_VERIFY_SHIELDED_TX_GAS; use namada_core::ledger::storage; +use namada_core::ledger::storage_api::token::read_denom; use namada_core::ledger::storage_api::{OptionExt, ResultExt}; use namada_core::ledger::vp_env::VpEnv; use namada_core::proto::Tx; @@ -328,12 +329,13 @@ where // Make a map to help recognize asset types lacking an epoch fn unepoched_tokens( token: &Address, -) -> Result> { + denom: token::Denomination, +) -> Result> { let mut unepoched_tokens = HashMap::new(); - for denom in MaspDenom::iter() { - let asset_type = encode_asset_type(None, token, denom) + for digit in MaspDenom::iter() { + let asset_type = encode_asset_type(None, token, denom, digit) .wrap_err("unable to create asset type")?; - unepoched_tokens.insert(asset_type, (token.clone(), denom)); + unepoched_tokens.insert(asset_type, (token.clone(), denom, digit)); } Ok(unepoched_tokens) } @@ -355,6 +357,10 @@ where let epoch = self.ctx.get_block_epoch()?; let conversion_state = self.ctx.storage.get_conversion_state(); let (transfer, shielded_tx) = self.ctx.get_shielded_action(tx_data)?; + let denom = read_denom(&self.ctx.pre(), &transfer.token)? + .ok_or_err_msg( + "No denomination found in storage for the given token", + )?; let transfer_amount = transfer .amount .to_amount(&transfer.token, &self.ctx.pre())?; @@ -385,7 +391,7 @@ where ripemd::Ripemd160::digest(sha2::Sha256::digest(&source_enc)); // To help recognize asset types not in the conversion tree - let unepoched_tokens = unepoched_tokens(&transfer.token)?; + let unepoched_tokens = unepoched_tokens(&transfer.token, denom)?; // Handle transparent input // The following boundary conditions must be satisfied // 1. Total of transparent input values equals containing transfer @@ -414,22 +420,32 @@ where // transparent inputs to a transaction for they would then // be able to claim rewards while locking their assets for // negligible time periods. - Some(((address, denom), asset_epoch, _, _)) - if *address == transfer.token - && *asset_epoch == epoch => + Some(( + (address, asset_denom, digit), + asset_epoch, + _, + _, + )) if *address == transfer.token + && *asset_denom == denom + && *asset_epoch == epoch => { total_in_values += token::Amount::from_masp_denominated( - vin.value, *denom, + vin.value, *digit, ); } // Maybe the asset type has no attached epoch None if unepoched_tokens.contains_key(&vin.asset_type) => { - let (token, denom) = &unepoched_tokens[&vin.asset_type]; + let (token, denom, digit) = + &unepoched_tokens[&vin.asset_type]; // Determine what the asset type would be if it were // epoched - let epoched_asset_type = - encode_asset_type(Some(epoch), token, *denom) - .wrap_err("unable to create asset type")?; + let epoched_asset_type = encode_asset_type( + Some(epoch), + token, + *denom, + *digit, + ) + .wrap_err("unable to create asset type")?; if conversion_state .assets .contains_key(&epoched_asset_type) @@ -444,7 +460,7 @@ where // trransparent input total_in_values += token::Amount::from_masp_denominated( - vin.value, *denom, + vin.value, *digit, ); } } @@ -510,7 +526,7 @@ where let hash = ripemd::Ripemd160::digest(sha2::Sha256::digest(&target_enc)); // To help recognize asset types not in the conversion tree - let unepoched_tokens = unepoched_tokens(&transfer.token)?; + let unepoched_tokens = unepoched_tokens(&transfer.token, denom)?; for out in &transp_bundle.vout { // Non-masp destinations subtract from transparent tx @@ -532,24 +548,29 @@ where } match conversion_state.assets.get(&out.asset_type) { // Satisfies 2. - Some(((address, denom), asset_epoch, _, _)) - if *address == transfer.token - && *asset_epoch <= epoch => + Some(( + (address, asset_denom, digit), + asset_epoch, + _, + _, + )) if *address == transfer.token + && *asset_denom == denom + && *asset_epoch <= epoch => { total_out_values += token::Amount::from_masp_denominated( - out.value, *denom, + out.value, *digit, ); } // Maybe the asset type has no attached epoch None if unepoched_tokens.contains_key(&out.asset_type) => { - let (_token, denom) = + let (_token, _denom, digit) = &unepoched_tokens[&out.asset_type]; // Otherwise note the contribution to this // trransparent input total_out_values += token::Amount::from_masp_denominated( - out.value, *denom, + out.value, *digit, ); } // unrecognized asset